"""Explicit operation counters for the measurement drivers kept with the
manuscripts (experiment E3).
The categories mirror the operation-count cost model that replaces
wall-clock seconds as the primary cost currency (Section 6 of the paper):
- :data:`SINGULAR_NODES` — singular-quadrature node evaluations (Duffy
triangle/cone nodes at which a radial profile or kernel is evaluated);
- :data:`SMOOTH_NODES` — smooth-rule (tensor Gauss-Legendre) node
evaluations of the windowed remainder;
- :data:`PROFILE_NODES` — nodes passed through a windowed channel profile
``psi_m`` (each such node costs one special-function-class evaluation);
- :data:`KERNEL_EVALS` — kernel evaluations, keyed by the kernel's special
function;
- :data:`SPECIAL_EVALS` — special-function evaluations keyed by function
(``expn``, ``erfc``, ``exp``, ``k0``, ``hankel1``, ...);
- :data:`RECOMBINATION_FLOPS` — recombination fused multiply-adds and
coefficient-recurrence steps of the assembly;
- :data:`TABLE_ENTRIES` — table entries built (one singular quadrature per
stored entry).
Counting is strictly explicit: code that owns a loop calls
:func:`add` (or ``OpCounters.add``) with the executed element count; nothing
is monkeypatched or inferred from timers. The module-level :func:`add` fans
out to every :class:`OpCounters` activated through the :func:`counting`
context manager and is a no-op (one truthiness check) when none is active,
so instrumented library code costs nothing outside instrumented runs.
The driver-level helpers at the bottom compute *analytic* counts from the
executed configuration — the surviving-node counts of the requested
quadrature rules, the symmetry-reduced entry set, the Duffy degeneracy
predicates, and the FMM traversal — so drivers can emit
measured-versus-analytic comparisons alongside their timing columns.
The last section adds the per-phase far-field counts of experiment E6:
:func:`fmm_stage_operation_counts` prices the seven FMM stages of one solve
from the traversal's interaction lists and the wrangler's expansion sizes,
so that a driver pairing it with
:mod:`volumential.phase_profile` can report a solve's per-phase shares in
operations and in seconds side by side.
"""
from __future__ import annotations
import operator
from contextlib import contextmanager
import numpy as np
__all__ = [
"FMM_FAR_FIELD_STAGES",
"KERNEL_EVALS",
"PROFILE_NODES",
"RECOMBINATION_FLOPS",
"SINGULAR_NODES",
"SMOOTH_NODES",
"SPECIAL_EVALS",
"TABLE_ENTRIES",
"OpCounters",
"add",
"batched_duffy_nodes_per_entry",
"counting",
"direct_build_fallback_reason",
"direct_build_routing",
"duffy_block_geometry",
"expansion_coefficient_counts",
"fmm_stage_operation_counts",
"fmm_stage_operation_counts_from_traversal",
"grouped_duffy_singular_nodes",
"nearfield_point_pairs",
"nearfield_point_pairs_from_counts",
"reduced_entry_count",
"regular_node_count",
"scalar_duffy_singular_nodes",
"surviving_radial_node_count",
]
#: Counter category for singular-quadrature node evaluations (Duffy
#: triangle/cone nodes at which a radial profile or kernel is evaluated).
SINGULAR_NODES = "singular_quadrature_nodes"
#: Counter category for smooth-rule (tensor Gauss-Legendre) node
#: evaluations of the windowed remainder.
SMOOTH_NODES = "smooth_rule_nodes"
#: Counter category for nodes passed through a windowed channel profile
#: ``psi_m``; each costs one special-function-class evaluation.
PROFILE_NODES = "channel_profile_nodes"
#: Counter category for kernel evaluations, keyed by the kernel's special
#: function.
KERNEL_EVALS = "kernel_evals"
#: Counter category for special-function evaluations keyed by function
#: (``expn``, ``erfc``, ``exp``, ``k0``, ``hankel1``, ...).
SPECIAL_EVALS = "special_function_evals"
#: Counter category for recombination fused multiply-adds and
#: coefficient-recurrence steps of the assembly.
RECOMBINATION_FLOPS = "recombination_flops"
#: Counter category for table entries built (one singular quadrature per
#: stored entry).
TABLE_ENTRIES = "table_entries_built"
def _require_count(count):
if isinstance(count, (bool, np.bool_)):
raise ValueError("count must be an integer")
try:
result = operator.index(count)
except TypeError as exc:
raise ValueError("count must be an integer") from exc
if result < 0:
raise ValueError("count must be >= 0")
return int(result)
[docs]
class OpCounters:
"""A bag of named operation counters, ``category -> label -> count``."""
def __init__(self):
self._counts: dict[str, dict[str, int]] = {}
[docs]
def add(self, category: str, label: str, count) -> None:
"""Add *count* to the counter named *label* within *category*."""
count = _require_count(count)
by_label = self._counts.setdefault(str(category), {})
by_label[str(label)] = by_label.get(str(label), 0) + count
[docs]
def labels(self, category: str) -> dict[str, int]:
"""Per-label counts of one category (a copy)."""
return dict(self._counts.get(str(category), {}))
[docs]
def total(self, category: str) -> int:
"""Sum of every label's count in *category*, zero if it has none."""
return sum(self._counts.get(str(category), {}).values())
[docs]
def as_dict(self) -> dict[str, dict[str, int]]:
"""A plain nested-dict copy of every counter, safe to serialize."""
return {
category: dict(by_label)
for category, by_label in self._counts.items()
}
[docs]
def by_function(self, category: str) -> str:
"""Compact ``label:count`` listing, ';'-joined, sorted by label."""
return ";".join(
f"{label}:{count}"
for label, count in sorted(self.labels(category).items())
)
_ACTIVE: list[OpCounters] = []
[docs]
@contextmanager
def counting(counters: OpCounters):
"""Activate ``counters`` for every :func:`add` call in the block."""
if not isinstance(counters, OpCounters):
raise ValueError("counting requires an OpCounters instance")
_ACTIVE.append(counters)
try:
yield counters
finally:
_ACTIVE.remove(counters)
[docs]
def add(category: str, label: str, count) -> None:
"""Increment every active counter; no-op when none is active."""
if not _ACTIVE:
return
for counters in _ACTIVE:
counters.add(category, label, count)
# {{{ executed build routing
[docs]
def direct_build_routing(table) -> str:
"""The DuffyRadial builder that actually produced ``table``'s data.
One of the recorded routings (``batched``, ``scalar``, ``scalar-adaptive``,
``scalar-fallback``), or ``unknown`` for a table whose payload predates the
recording. Read the *recorded* routing rather than re-evaluating the
routing predicate: ``_supports_batched_duffy_builder`` reports what was
attempted, and a batched build that failed and fell back to the scalar
per-entry builder has a different cost class and a different converged
accuracy at fixed orders. A ``scalar-fallback`` here invalidates the
batched node counts of the cost model for that table.
"""
routing = getattr(table, "build_routing", None)
if routing is None:
return "unknown"
return str(routing)
[docs]
def direct_build_fallback_reason(table) -> str:
"""``"<ExceptionType>: <message>"`` of a recorded scalar fallback, else
the empty string."""
reason = getattr(table, "build_fallback_reason", None)
if reason is None:
return ""
return str(reason)
# }}}
# {{{ analytic counts from the executed configuration
[docs]
def surviving_radial_node_count(
radial_order, *, radial_rule="tanh-sinh-fast", mp_dps=50
) -> int:
"""Surviving nodes of the requested radial rule (float64-saturating
tanh-sinh nodes are masked by the builder, so the count is measured by
executing the node builder, not assumed from the requested order)."""
import volumential.singular_integral_2d as squad
nodes, _ = squad._duffy_radial_nodes_weights(
radial_rule, int(radial_order), int(mp_dps)
)
return int(nodes.size)
[docs]
def regular_node_count(dim, regular_order) -> int:
"""Regular (non-radial) node count of one Duffy region: the angular
Gauss order in 2D, the two-axis tensor count in 3D — measured from the
same node builder the quadratures use."""
dim = int(dim)
if dim not in (2, 3):
raise NotImplementedError("Duffy node counting supports 2D and 3D")
if dim == 2:
import scipy.special as sps
return int(sps.roots_legendre(int(regular_order))[0].size)
import volumential.singular_integral_2d as squad
nodes, _ = squad._duffy_regular_nodes_weights(dim - 1, int(regular_order))
return int(np.asarray(nodes).shape[0])
[docs]
def batched_duffy_nodes_per_entry(
dim, regular_order, radial_order, *, radial_rule="tanh-sinh-fast"
) -> int:
"""Node-template size of the batched direct builder per reduced entry:
``2^d d! * regular nodes * surviving radial nodes`` (degenerate
sign-octants keep their nodes in the generated code)."""
from math import factorial
dim = int(dim)
if dim not in (2, 3):
raise NotImplementedError("Duffy node counting supports 2D and 3D")
return (
(2**dim)
* factorial(dim)
* regular_node_count(dim, regular_order)
* surviving_radial_node_count(radial_order, radial_rule=radial_rule)
)
[docs]
def reduced_entry_count(table) -> int:
"""Number of symmetry-reduced (quadratured) entries of a table."""
return int(
np.asarray(table._get_invariant_entry_info()["entry_ids"]).size
)
[docs]
def duffy_block_geometry(table) -> dict:
"""Counts of the grouped Duffy entry enumeration for one table geometry.
Mirrors the region-degeneracy predicates of the grouped channel builder
(``volumential.rke_table_assembly._duffy_channel_entry_values``),
which agree with the scalar builder's collinear-triangle skip: 2D
triangles are dropped when ``|det| < 1e-14 extent^2``, 3D sign-octants
when any edge length vanishes (each surviving octant contributes the
``d!`` axis-permutation cones).
:returns: dict with ``n_reduced_entries``, ``n_blocks`` (distinct
(case, target) blocks), ``n_active_regions`` (non-degenerate
triangles/cones summed over blocks — the grouped builders evaluate
each region once per block), and ``entry_weighted_active_regions``
(the same regions summed per member entry — the scalar per-entry
builder pays each region once per entry).
"""
from itertools import product as iproduct
from volumential.rke_table_assembly import _reduced_entry_groups
dim = int(table.dim)
if dim not in (2, 3):
raise NotImplementedError("Duffy node counting supports 2D and 3D")
extent = float(table.source_box_extent)
entry_ids, groups = _reduced_entry_groups(table)
n_active_regions = 0
entry_weighted = 0
for (case_index, target_index), members in groups.items():
target = np.asarray(
table.find_target_point(target_index, case_index),
dtype=np.float64,
)
singular = np.clip(target, 0.0, extent)
if dim == 2:
corners = [
np.array([0.0, 0.0]),
np.array([extent, 0.0]),
np.array([extent, extent]),
np.array([0.0, extent]),
]
active = 0
for corner_index in range(4):
edge1 = corners[corner_index] - singular
edge2 = corners[(corner_index + 1) % 4] - singular
det = edge1[0] * edge2[1] - edge1[1] * edge2[0]
if np.abs(det) >= 1.0e-14 * extent * extent:
active += 1
else:
active = 0
for signs in iproduct((-1.0, 1.0), repeat=dim):
lengths = [
singular[axis] if signs[axis] < 0
else extent - singular[axis]
for axis in range(dim)
]
if all(length > 0.0 for length in lengths):
active += 6 # the 3! axis-permutation cones
n_active_regions += active
entry_weighted += active * len(members)
return {
"n_reduced_entries": int(entry_ids.size),
"n_blocks": len(groups),
"n_active_regions": int(n_active_regions),
"entry_weighted_active_regions": int(entry_weighted),
}
[docs]
def grouped_duffy_singular_nodes(
table, regular_order, radial_order, *, geometry=None
) -> int:
"""Singular-quadrature nodes of one grouped (per-block) build over the
reduced entry set — what one windowed/complex channel build evaluates."""
if geometry is None:
geometry = duffy_block_geometry(table)
return geometry["n_active_regions"] * regular_node_count(
int(table.dim), regular_order
) * surviving_radial_node_count(radial_order)
[docs]
def scalar_duffy_singular_nodes(
table, regular_order, radial_order, *, geometry=None
) -> int:
"""Singular-quadrature nodes of one scalar (per-entry) build over the
reduced entry set — the legacy interpreted path pays every region once
per member entry rather than once per block."""
if geometry is None:
geometry = duffy_block_geometry(table)
return geometry["entry_weighted_active_regions"] * regular_node_count(
int(table.dim), regular_order
) * surviving_radial_node_count(radial_order)
[docs]
def nearfield_point_pairs_from_counts(
*,
target_boxes,
neighbor_source_boxes_starts,
neighbor_source_boxes_lists,
box_target_counts_nonchild,
box_source_counts_nonchild=None,
) -> int:
"""Near-field (target point, source point) pairs of one List 1 pass.
For every target box, its targets times the source points of its
neighbor source boxes. ``box_source_counts_nonchild`` defaults to the
target counts, which is the box-FMM near field's own convention (a
box's sources are its target quadrature points). Pass an explicit
source-count array to price a pass that runs over a *different* source
set — the online split remainder does exactly that when it evaluates on
an interpolated smooth quadrature instead of the base nodes.
"""
target_boxes = np.asarray(target_boxes, dtype=np.int64)
starts = np.asarray(neighbor_source_boxes_starts, dtype=np.int64)
lists = np.asarray(neighbor_source_boxes_lists, dtype=np.int64)
target_counts = np.asarray(box_target_counts_nonchild, dtype=np.int64)
if box_source_counts_nonchild is None:
source_counts = target_counts
else:
source_counts = np.asarray(box_source_counts_nonchild, dtype=np.int64)
if target_boxes.size == 0:
return 0
per_box_sources = _csr_group_sums(
source_counts[lists] if lists.size else lists, starts
)
return int(np.sum(target_counts[target_boxes] * per_box_sources))
[docs]
def nearfield_point_pairs(queue, traversal) -> int:
"""Near-field (target point, source quadrature point) pairs of one List 1
pass: for every target box, its targets times the quadrature points of
its neighbor source boxes. In the box-FMM near field, sources of a box
are its target quadrature points (``box_target_counts_nonchild`` serves
both sides), matching the wrangler's near-field kernel launch."""
return nearfield_point_pairs_from_counts(
target_boxes=traversal.target_boxes.get(queue),
neighbor_source_boxes_starts=(
traversal.neighbor_source_boxes_starts.get(queue)
),
neighbor_source_boxes_lists=(
traversal.neighbor_source_boxes_lists.get(queue)
),
box_target_counts_nonchild=(
traversal.tree.box_target_counts_nonchild.get(queue)
),
)
# }}}
# {{{ per-phase FMM stage operation counts (E6)
#: FMM far-field stage names, matching
#: :data:`volumential.phase_profile.FAR_FIELD_PHASES` without the ``far_``
#: prefix and the order in which ``drive_volume_fmm`` runs them.
FMM_FAR_FIELD_STAGES = (
"form_multipoles",
"coarsen_multipoles",
"multipole_to_local",
"eval_multipoles",
"form_locals",
"refine_locals",
"eval_locals",
)
def _host(array, queue):
"""Host copy of a device or host array, without assuming which it is."""
if array is None:
return None
if hasattr(array, "get"):
return np.asarray(array.get(queue))
return np.asarray(array)
def _csr_group_sums(values, starts):
"""Per-group sums of ``values`` under the CSR ``starts``, as int64.
Handles empty groups and an empty ``lists`` array, which ``reduceat``
does not.
"""
starts = np.asarray(starts, dtype=np.int64)
if starts.size == 0:
return np.zeros(0, dtype=np.int64)
values = np.asarray(values, dtype=np.int64)
cumulative = np.concatenate(
[np.zeros(1, dtype=np.int64), np.cumsum(values, dtype=np.int64)]
)
return cumulative[starts[1:]] - cumulative[starts[:-1]]
[docs]
def fmm_stage_operation_counts(
*,
box_levels,
box_source_counts_nonchild,
box_target_counts_nonchild,
box_child_ids,
source_boxes,
source_parent_boxes,
target_boxes,
target_or_target_parent_boxes,
from_sep_siblings_starts,
from_sep_bigger_starts,
from_sep_bigger_lists,
sep_smaller_by_level,
multipole_coeff_counts,
local_coeff_counts,
) -> dict[str, int]:
"""Operation counts of the seven FMM far-field stages of one solve.
Every count is a number of *coefficient touches*: one multiply-add
against one expansion coefficient (or, for P2M/L2P/P2L, one
source/target against one coefficient). This is the dense-translation
structural model, instantiated from the executed traversal's own
interaction lists and the executed wrangler's own per-level expansion
sizes -- nothing here is a constant, and nothing is a flop count of the
generated OpenCL: a translation that sumpy accelerates (FFT-based or
translation-class M2L, rotation-based operators) executes fewer machine
operations than the dense count reported here, while still touching the
same coefficients. Consumers must read the counts as a structural
decomposition of the stage graph, not as a hardware work estimate.
The stage-by-stage rules follow the loops of
:class:`sumpy.fmm.SumpyExpansionWrangler` at the executed revision, so
where the textbook model and the implementation disagree the
implementation wins. Two such disagreements are reproduced here:
* ``coarsen_multipoles`` (M2M) runs only for source levels
``nlevels-1 ... 3`` (``sumpy.fmm.SumpyExpansionWrangler
.coarsen_multipoles``), because no level-1 box is well separated from
another; translations into levels 0 and 1 are never performed and are
therefore not counted.
* ``refine_locals`` (L2L) is driven by the *target* box list, one
parent-to-child translation per target-or-target-parent box at level
``>= 1``, not by an enumeration of children.
:arg box_levels: ``tree.box_levels``, host array.
:arg box_source_counts_nonchild: ``tree.box_source_counts_nonchild``.
:arg box_target_counts_nonchild: ``tree.box_target_counts_nonchild``.
:arg box_child_ids: ``tree.box_child_ids``, shape ``(2**dim, nboxes)``;
a zero entry means "no such child" (boxtree convention).
:arg source_boxes: ``traversal.source_boxes``.
:arg source_parent_boxes: ``traversal.source_parent_boxes``.
:arg target_boxes: ``traversal.target_boxes``.
:arg target_or_target_parent_boxes:
``traversal.target_or_target_parent_boxes``.
:arg from_sep_siblings_starts: List 2 CSR starts, indexed like
``target_or_target_parent_boxes``.
:arg from_sep_bigger_starts: List 4 CSR starts, indexed like
``target_or_target_parent_boxes``.
:arg from_sep_bigger_lists: List 4 CSR source-box lists.
:arg sep_smaller_by_level: one ``(target_boxes, starts)`` pair per
source level, from ``traversal.target_boxes_sep_smaller_by_source_level``
and ``traversal.from_sep_smaller_by_level[i].starts`` (List 3 far).
:arg multipole_coeff_counts: multipole coefficients per tree level.
:arg local_coeff_counts: local coefficients per tree level.
:returns: a dict with one key per entry of :data:`FMM_FAR_FIELD_STAGES`
plus ``far_total``.
"""
box_levels = np.asarray(box_levels, dtype=np.int64)
nsources = np.asarray(box_source_counts_nonchild, dtype=np.int64)
ntargets = np.asarray(box_target_counts_nonchild, dtype=np.int64)
child_ids = np.asarray(box_child_ids, dtype=np.int64)
source_boxes = np.asarray(source_boxes, dtype=np.int64)
source_parent_boxes = np.asarray(source_parent_boxes, dtype=np.int64)
target_boxes = np.asarray(target_boxes, dtype=np.int64)
totpb = np.asarray(target_or_target_parent_boxes, dtype=np.int64)
n_mpole = np.asarray(multipole_coeff_counts, dtype=np.int64)
n_local = np.asarray(local_coeff_counts, dtype=np.int64)
nlevels = int(n_mpole.size)
if int(n_local.size) != nlevels:
raise ValueError(
"multipole and local coefficient counts must both be per level"
)
# P2M: every source of a source box enters every multipole coefficient
# of that box's level.
p2m = int(
np.sum(nsources[source_boxes] * n_mpole[box_levels[source_boxes]])
)
# M2M: one dense child-to-parent translation per existing child of every
# source parent box, for the source levels sumpy actually visits.
m2m = 0
for source_level in range(nlevels - 1, 2, -1):
target_level = source_level - 1
parents = source_parent_boxes[
box_levels[source_parent_boxes] == target_level
]
if parents.size == 0:
continue
n_children = int(np.count_nonzero(child_ids[:, parents]))
m2m += n_children * int(n_mpole[source_level]) * int(
n_mpole[target_level]
)
# M2L: one dense multipole-to-local translation per List 2 entry, source
# and target on the same level.
sib_starts = np.asarray(from_sep_siblings_starts, dtype=np.int64)
n_list2 = sib_starts[1:] - sib_starts[:-1]
totpb_levels = box_levels[totpb]
m2l = int(
np.sum(n_list2 * n_mpole[totpb_levels] * n_local[totpb_levels])
)
# M2P (List 3 far): every target of the target box is evaluated from
# every listed multipole expansion at the source level.
m2p = 0
for source_level, (level_target_boxes, starts) in enumerate(
sep_smaller_by_level
):
level_target_boxes = np.asarray(level_target_boxes, dtype=np.int64)
if level_target_boxes.size == 0:
continue
starts = np.asarray(starts, dtype=np.int64)
n_list3 = starts[1:] - starts[:-1]
m2p += int(
np.sum(ntargets[level_target_boxes] * n_list3)
* int(n_mpole[source_level])
)
# P2L (List 4 far): every source of every listed bigger box enters every
# local coefficient of the target box's level.
bigger_lists = np.asarray(from_sep_bigger_lists, dtype=np.int64)
n_list4_sources = _csr_group_sums(
nsources[bigger_lists] if bigger_lists.size else bigger_lists,
from_sep_bigger_starts,
)
p2l = int(np.sum(n_list4_sources * n_local[totpb_levels]))
# L2L: one dense parent-to-child translation per target-or-target-parent
# box at level >= 1.
l2l = 0
for target_level in range(1, nlevels):
n_boxes = int(np.count_nonzero(totpb_levels == target_level))
if n_boxes == 0:
continue
l2l += n_boxes * int(n_local[target_level - 1]) * int(
n_local[target_level]
)
# L2P: every target reads every local coefficient of its box's level.
l2p = int(
np.sum(ntargets[target_boxes] * n_local[box_levels[target_boxes]])
)
counts = {
"form_multipoles": p2m,
"coarsen_multipoles": m2m,
"multipole_to_local": m2l,
"eval_multipoles": m2p,
"form_locals": p2l,
"refine_locals": l2l,
"eval_locals": l2p,
}
counts["far_total"] = int(sum(counts.values()))
return counts
[docs]
def expansion_coefficient_counts(wrangler) -> tuple[list[int], list[int]]:
"""Per-level ``(multipole, local)`` coefficient counts of a wrangler.
Read off the executed expansion classes at the executed per-level
orders, so a kernel-specific expansion (the 2D Helmholtz/Yukawa Bessel
expansions, say, at ``2 * order + 1`` coefficients) is priced as the run
actually priced it rather than by a Taylor-order formula.
"""
tree_indep = wrangler.tree_indep
orders = [int(order) for order in wrangler.level_orders]
multipole = [len(tree_indep.multipole_expansion(order)) for order in orders]
local = [len(tree_indep.local_expansion(order)) for order in orders]
return multipole, local
[docs]
def fmm_stage_operation_counts_from_traversal(
queue, traversal, wrangler
) -> dict[str, int]:
""":func:`fmm_stage_operation_counts` for an executed traversal/wrangler."""
tree = traversal.tree
multipole, local = expansion_coefficient_counts(wrangler)
sep_smaller_by_level = []
level_target_boxes = getattr(
traversal, "target_boxes_sep_smaller_by_source_level", None
)
from_sep_smaller = getattr(traversal, "from_sep_smaller_by_level", None)
if level_target_boxes is not None and from_sep_smaller is not None:
for boxes, ssn in zip(level_target_boxes, from_sep_smaller, strict=True):
host_boxes = _host(boxes, queue)
if host_boxes is None or host_boxes.size == 0:
sep_smaller_by_level.append(
(np.zeros(0, dtype=np.int64), np.zeros(1, dtype=np.int64))
)
continue
sep_smaller_by_level.append(
(host_boxes, _host(ssn.starts, queue))
)
else:
sep_smaller_by_level = [
(np.zeros(0, dtype=np.int64), np.zeros(1, dtype=np.int64))
for _ in range(len(multipole))
]
bigger_starts = _host(
getattr(traversal, "from_sep_bigger_starts", None), queue
)
bigger_lists = _host(
getattr(traversal, "from_sep_bigger_lists", None), queue
)
if bigger_starts is None:
bigger_starts = np.zeros(1, dtype=np.int64)
bigger_lists = np.zeros(0, dtype=np.int64)
return fmm_stage_operation_counts(
box_levels=_host(tree.box_levels, queue),
box_source_counts_nonchild=_host(
tree.box_source_counts_nonchild, queue
),
box_target_counts_nonchild=_host(
tree.box_target_counts_nonchild, queue
),
box_child_ids=_host(tree.box_child_ids, queue),
source_boxes=_host(traversal.source_boxes, queue),
source_parent_boxes=_host(traversal.source_parent_boxes, queue),
target_boxes=_host(traversal.target_boxes, queue),
target_or_target_parent_boxes=_host(
traversal.target_or_target_parent_boxes, queue
),
from_sep_siblings_starts=_host(
traversal.from_sep_siblings_starts, queue
),
from_sep_bigger_starts=bigger_starts,
from_sep_bigger_lists=bigger_lists,
sep_smaller_by_level=sep_smaller_by_level,
multipole_coeff_counts=multipole,
local_coeff_counts=local,
)
# }}}