22 """Return the inner-most singly-jagged ListOffsetArray of ``array``.
24 For a ``var * var * T`` input (e.g. NumTrkPt500), returns a view whose
25 layout is a ``ListOffsetArray`` with ``NumpyArray`` content — i.e. the
26 per-particle list structure collapsed across the full event range.
27 Use ``result.layout.offsets.data`` for cumulative per-particle inner
28 offsets and ``result.layout.content.data`` for the flat numeric buffer.
30 Solution from Peter Fackeldey: traverse the layout with ``ak.transform``
31 and capture the deepest singly-jagged node.
34 layout_depth = layout.purelist_depth
36 is_branched, _ = layout.branch_depth
39 f
"{layout} has branching, cannot extract inner-most ListOffsetArray"
42 def _is_singly_jagged(lay):
44 isinstance(lay, ak.contents.ListOffsetArray)
45 and isinstance(lay.content, ak.contents.NumpyArray)
50 def _capture(lay, depth, **_kwargs):
52 if depth == (layout_depth - 1)
and _is_singly_jagged(lay):
53 found = lay.materialize()
55 ak.transform(_capture, layout, return_value=
"none")
59 "Did not find a singly-jagged ListOffsetArray at the inner-most depth"
61 return ak.Array(found)
163 """Convert per-event link indices to global offsets into the target.
165 Mirrors ``LinkColumnVector::addLink``/``addSplitLink`` in
166 ColumnarTestFixtures/Root/ColumnarPhysliteTest.cxx: a stored
167 ``(m_persKey == 0, m_persIndex == 0)`` pair (the Athena persistent
168 null encoding) or ``m_persIndex == 0xFFFFFFFF`` (the standalone
169 ``ElementLinkBase::isDefault`` encoding) marks a null link, which
170 becomes ``invalid_link_value``; every other link is offset by the
171 target container's per-event start and bounds-checked against the
172 per-event end. The m_persKey value itself is not validated against the
173 target container (see the sg_key binding for computing expected keys).
175 ``index``/``key`` may be jagged at depth 2 (one link per object) or
176 depth 3 (a vector of links per object); the per-event ``target_offsets``
177 broadcast down either structure.
179 starts = np.asarray(target_offsets)[:-1]
180 ends = np.asarray(target_offsets)[1:]
181 global_index = ak.values_astype(index, np.uint64) + starts
182 valid = ~(((key == 0) & (index == 0)) | (index == 0xFFFFFFFF))
183 out_of_range = valid & (global_index >= ends)
184 if ak.any(out_of_range, axis=
None):
186 f
"link index out of range for column '{col.name}' "
187 f
"targeting '{col.sole_link_target_name}'"
190 {int(k)
for k
in ak.flatten(key[out_of_range], axis=
None).to_list()}
192 msg += f
" (m_persKey of offending links: {[hex(k) for k in found]}"
194 if expected
is not None:
195 msg += f
", expected 0x{expected:08x}"
196 raise RuntimeError(msg +
")")
197 return ak.where(valid, global_index, np.uint64(invalid_link_value))
201 """Flatten a one-link-per-object column to a uint64 global-offset buffer."""
204 return np.ascontiguousarray(
205 ak.to_numpy(ak.flatten(converted, axis=1)), dtype=np.uint64
210 """Convert a vector-of-links column to (nested offsets, data) buffers.
212 The returned offsets have one entry per object plus one (the nested
213 ``.offset`` column); the data buffer holds the flattened uint64 global
214 offsets in object order.
219 counts = ak.to_numpy(ak.flatten(ak.num(converted, axis=2), axis=1))
221 data = np.ascontiguousarray(
222 ak.to_numpy(ak.flatten(converted, axis=
None)), dtype=np.uint64
228 """Group ColumnInfo objects by container and role.
233 Iterable of ColumnInfo objects as returned by PythonToolHandle.columns.
238 Keyed by container offset name (e.g. "EventInfo", "Muons"). Each value
241 - ``"offset"``: the ColumnInfo for this container's offset column
242 - ``"inputs"``: list of input ColumnInfo belonging to this container
243 - ``"outputs"``: list of output ColumnInfo belonging to this container
244 - ``"nested_offsets"``: dict of name -> ColumnInfo for offset columns
245 that are children of this container (e.g. "Muons.NumTrkPt500.offset")
249 Container offsets have ``is_offset=True`` and an ``offset_name`` of either
250 ``''`` (root, e.g. "EventInfo") or the name of another container offset
251 (e.g. "Muons" has ``offset_name="EventInfo"``). Nested-vector offsets also
252 have ``is_offset=True`` but their name contains a dot; they are stored under
253 ``"nested_offsets"`` of their parent container rather than as top-level keys.
256 offset_cols = {col.name: col
for col
in columns
if col.is_offset}
257 data_cols = [col
for col
in columns
if not col.is_offset]
266 container_offsets = {}
267 nested_offsets_by_container = {}
269 for name, col
in offset_cols.items():
270 parent = col.offset_name
271 if parent ==
"" or parent
in offset_cols:
274 nested_offsets_by_container.setdefault(parent, {})[name] = {
280 container_offsets[name] = col
283 container_offsets[name] = col
291 "nested_offsets": nested_offsets_by_container.get(name, {}),
293 for name, col
in container_offsets.items()
298 nested_to_container = {
299 nested_name: container
300 for container, nested_map
in nested_offsets_by_container.items()
301 for nested_name
in nested_map
305 for col
in data_cols:
306 target = col.offset_name
307 is_output = col.access_mode == ColumnAccessMode.output
309 if target
in classified:
310 bucket = classified[target]
311 elif target
in nested_to_container:
312 container = nested_to_container[target]
313 bucket = classified[container][
"nested_offsets"][target]
319 bucket[
"outputs"].append(col)
321 bucket[
"inputs"].append(col)
371 """Extract flat numpy buffers from an awkward array.
373 Returns a dict mapping column name -> numpy array, covering all container
374 offsets, nested-vector offsets, and input data columns. Output column
375 buffers are not included (allocate_outputs handles those).
380 An ak.Array (real or zero-length after typetracer conversion).
382 Output of classify_columns or resolve_optional_columns.
385 num_events = int(ak.num(events, axis=0))
386 synthesized_offsets =
set()
392 for container_name, info
in classified.items():
393 nested_offsets = info[
"nested_offsets"]
397 for nested_offset_name, nested
in nested_offsets.items():
398 for col
in nested[
"inputs"]:
399 if col.is_variant_link:
400 raise NotImplementedError(
401 f
"variant link columns are not supported "
402 f
"(column '{col.name}')"
405 deferred_links.append((nested_offset_name, col))
409 raw_offsets = np.asarray(inner.layout.offsets.data)
410 start = int(raw_offsets[0])
411 end = int(raw_offsets[-1])
414 buffers[nested_offset_name] = np.ascontiguousarray(
415 raw_offsets - start, dtype=np.uint64
417 buffers[col.name] = np.ascontiguousarray(
418 inner.layout.content.data[start:end]
421 for col
in info[
"inputs"]:
422 if col.is_variant_link:
423 raise NotImplementedError(
424 f
"variant link columns are not supported (column '{col.name}')"
427 flat_inputs = [col
for col
in info[
"inputs"]
if not _is_link_column(col)]
428 deferred_links.extend((container_name, col)
for col
in link_inputs)
430 if not flat_inputs
and not nested_offsets
and not link_inputs:
432 buffers[container_name] = np.array([0, num_events], dtype=np.uint64)
433 synthesized_offsets.add(container_name)
439 any_nested_input = next(
440 (col
for nested
in nested_offsets.values()
for col
in nested[
"inputs"]),
443 if any_nested_input
is not None:
445 jagged = events[base]
448 events, link_inputs[0].name, link_inputs[0].name
452 if jagged
is not None:
454 ak.to_numpy(ak.num(jagged, axis=1))
457 buffers[container_name] = np.array([0, num_events], dtype=np.uint64)
458 synthesized_offsets.add(container_name)
463 sorted_cols = sorted(flat_inputs, key=
lambda c: c.offset_name)
465 for offset_name, cols_iter
in itertools.groupby(
466 sorted_cols, key=
lambda c: c.offset_name
468 cols = list(cols_iter)
469 unzipped = {col.name: events[col.name]
for col
in cols}
470 zipped = ak.zip(unzipped)
473 form, length, raw_buffers = ak.to_buffers(
474 zipped, form_key=f
"{offset_name}{{id}}"
477 if isinstance(form, ak.forms.RecordForm):
482 buffers[container_name] = np.array(
483 [0, length], dtype=np.uint64
486 buffers[col.name] = ak.to_numpy(events[col.name])
487 elif isinstance(form, ak.forms.ListOffsetForm):
491 key
for key
in raw_buffers
if key.endswith(
"-offsets")
493 buffers[container_name] = np.asarray(
494 raw_buffers[offset_key]
499 for field
in inner.fields:
500 buffers[field] = np.asarray(
501 raw_buffers[f
"{inner.content(field).form_key}-data"]
505 f
"Cannot handle form {type(form)} for "
506 f
"container {container_name}"
510 for offset_owner, col
in deferred_links:
511 target = col.sole_link_target_name
512 target_offsets = buffers.get(target)
513 if target_offsets
is None or target
in synthesized_offsets:
515 f
"link column '{col.name}' targets container '{target}', "
516 "whose offsets could not be derived from the tool's input columns"
518 if col.name.endswith(
".data"):
520 events, col, target_offsets
522 buffers[offset_owner] = nested_link_offsets
523 buffers[col.name] = data
526 events, col, target_offsets
533 """Allocate zero-filled numpy arrays for each output column.
535 Sizes each output array using ``offsets[-1]`` of the referenced offset
536 buffer. Arrays are added into ``buffer_dict`` in-place and also returned.
541 Output of ``classify_columns`` or ``resolve_optional_columns``.
543 Dict of column name -> numpy array, as returned by ``extract_buffers``.
544 Modified in-place to include the newly allocated output arrays.
549 Mapping of output column name -> zero-filled numpy array (same objects
550 also inserted into ``buffer_dict``).
552 nested_offset_names = {
554 for info
in classified.values()
555 for nested_name
in info[
"nested_offsets"]
558 for _container_name, info
in classified.items():
559 for col
in info[
"outputs"]:
560 if col.offset_name
in nested_offset_names:
561 raise NotImplementedError(
562 f
"Nested-vector output columns are not supported "
563 f
"(column '{col.name}' has nested offset '{col.offset_name}')"
565 offset_data = buffer_dict.get(col.offset_name)
566 if offset_data
is None:
568 f
"Cannot find offset buffer '{col.offset_name}' "
569 f
"needed for output column '{col.name}'"
571 raise RuntimeError(msg)
572 size = int(offset_data[-1])
573 arr = np.zeros(size, dtype=col.dtype)
574 output_buffers[col.name] = arr
575 buffer_dict[col.name] = arr
576 return output_buffers
580 """Build an awkward array from output column buffers.
585 Output of ``classify_columns`` or ``resolve_optional_columns``.
587 Dict of column name -> numpy array, containing both offset buffers and
588 the output arrays populated by ``allocate_outputs`` and ``call()``.
590 Number of events (outer axis length of the returned array).
595 Record array with one field per output column, each a variable-length
596 list of per-particle values (i.e. ``var * dtype``).
604 for _container_name, info
in classified.items():
605 for col
in info[
"outputs"]:
606 node_offset = f
"node{2 * node_index}"
607 node_data = f
"node{2 * node_index + 1}"
610 form_fields.append(col.name)
611 form_contents.append(
613 "class":
"ListOffsetArray",
616 "class":
"NumpyArray",
617 "primitive": col.dtype,
618 "form_key": node_data,
620 "form_key": node_offset,
624 out_buffers[f
"{node_data}-data"] = buffer_dict[col.name]
625 out_buffers[f
"{node_offset}-offsets"] = buffer_dict[col.offset_name]
628 "class":
"RecordArray",
629 "fields": form_fields,
630 "contents": form_contents,
634 return ak.from_buffers(form, num_events, out_buffers)