Skip to content

N-dimensional execution spine: flow zarr/netcdf through beacon.nd-encoded batches#328

Merged
robinskil merged 10 commits into
mainfrom
features/refactor-into-nd-execs
Jul 11, 2026
Merged

N-dimensional execution spine: flow zarr/netcdf through beacon.nd-encoded batches#328
robinskil merged 10 commits into
mainfrom
features/refactor-into-nd-execs

Conversation

@robinskil

Copy link
Copy Markdown
Collaborator

Summary

Introduces a self-contained N-dimensional execution spine in beacon-datafusion-ext::nd and wires the netcdf and zarr readers through it. N-dimensional data now travels up the DataFusion plan as a real Arrow type (beacon.nd-encoded structs) carried by a standard DataSourceExec, with two dedicated plan nodes on top that decode and flatten it into an ordinary table.

The pipeline

NdBroadcastExec          ← flattens nd arrays → a plain Arrow table
   └─ NdSourceExec        ← decodes beacon.nd structs → in-memory NdRecordBatch
        └─ DataSourceExec ← standard DF scan; its output schema is ENCODED
             └─ FileOpener ← reads the dataset, emits beacon.nd batches

Each nd variable (plus its named dimensions) is packed into one beacon.nd struct row — Struct{ values: List<T>, dim_sizes: List<UInt32>, dim_names: List<Utf8> } — so a batch of them rides through DataSourceExec like any other RecordBatch, keeping partitioning, projection pushdown, caching, and file-stats machinery. NdSourceExec decodes them into named-dimension arrays; NdBroadcastExec broadcasts each onto the shared grid (a single Arrow take, or a zero-copy clone on the identity) to produce the flat table the rest of the query sees. Both nodes carry DataFusion metrics.

What's included

  • beacon-datafusion-ext/src/nd/ — the spine: Dimensions, NdArrowArray (flat values + named dims), BroadcastMap (name-aligned/xarray-style broadcast as per-axis strides), NdRecordBatch, the beacon.nd Arrow encoding (encoding.rs), and the NdSourceExec/NdBroadcastExec plan nodes.
  • beacon-nd-array as an nd batch provider — chunks a regular dataset C-order into beacon.nd-encoded batches; ragged (CF contiguous) datasets fall back to run-length expansion wrapped on a synthetic row dim (identity broadcast).
  • netcdf + zarr wired end-to-end — openers emit encoded batches and adapt in the encoded (struct) domain; create_physical_plan returns NdBroadcastExec(NdSourceExec(DataSourceExec)).
  • Metadata attributes as rank-0 scalars — netcdf/zarr variable/global attributes (<var>.<attr>, .<global>) flow through as rank-0 beacon.nd columns that broadcast to constant columns across the grid.
  • CF scale_factor/add_offset in the netcdf reader — new ScaleOffsetVariableDecoder decodes packed variables to f64 (raw*scale+offset), with the fill value decoded by the same arithmetic so packed fill cells null out. This aligns netcdf with the zarr reader (both now decode scaled variables to Float64).

Tests

End-to-end coverage for both formats:

  • Plan shape — asserts the NdBroadcastExec → NdSourceExec → DataSourceExec nesting.
  • Variables + attributes — a gridded variable comes back decoded (Float64) and rank-0 attributes broadcast to constant columns on every row.
  • Broadcast correctness — a rank-0 attribute has exactly one distinct value across the full grid and is present on every row.
  • Scale/offset — decoded SST lands in a physical kelvin range (raw packed int16 never would), plus decoder unit tests.

Full workspace: 819 passed / 0 failed.

Deferred (follow-ups)

  • Filter/projection execs + optimizer on the spine (filter on un-broadcast coordinate axes to avoid materializing the cross-product) — currently filters sit above NdBroadcastExec as inexact FilterExec.
  • REE / lazy broadcast output for replicated axes.
  • True run-length ragged broadcast (currently an identity round-trip).
  • valid_min/valid_max range masking (neither reader applies it today).

robinskil added 10 commits July 10, 2026 10:41
Introduce beacon-datafusion-ext::nd, a self-contained library for
N-dimensional Arrow arrays and dimension-aware physical operators.

- NdArrowArray: a flat Arrow array plus named C-order Dimensions;
  zero-copy subset and broadcast-composed materialize.
- BroadcastMap: name-aligned broadcast expressed as stride arithmetic.
- NdRecordBatch: columns (each on a subset of the dimensions) over a
  shared target grid.
- NdSourceExec (+ NdBatchProvider) streams nd batches via an execute_nd
  side channel; NdBroadcastExec materializes them into flat RecordBatches.

Filtering and projection operators will layer on top of this spine
later. Depends only on arrow + datafusion; tested against an in-memory
provider (31 tests).
- NdSourceExec::execute now delegates to NdBroadcastExec instead of
  materializing itself, so flattening lives in one place; the source is
  a pure nd-batch producer via execute_nd.
- Add ExecutionPlanMetricsSet to both nodes (metrics() override):
  NdSourceExec records output grid rows, an nd_batches counter, and
  elapsed time; NdBroadcastExec records flattened output rows and
  materialization time via BaselineMetrics threaded through
  materialize_nd_stream.
Remove functions left dead after dropping filter/projection:
- array.rs: ArraySubset + NdArrowArray::subset/contiguous_range,
  try_new_scalar, and the values/dims/shape/num_elements accessors;
  materialize loses its always-None selection parameter.
- broadcast.rs: run_structure/to_run_array/RunStructure (REE),
  source_index_of, the target_shape accessor, and gather_indices'
  selection parameter; num_target_elements is now private.
- dimensions.rs: Dimension::name_arc and Dimensions::is_scalar.
- mod.rs: drop ArraySubset/RunStructure from the re-exports.

These will return alongside filtering/projection. 23 tests pass.
DatasetNdProvider implements beacon-datafusion-ext's NdBatchProvider:
it chunks a regular Dataset in C-order and yields one NdRecordBatch per
chunk with each variable left un-broadcast, so a format can feed a
dataset into NdSourceExec (with NdBroadcastExec materializing above).

- add beacon-datafusion-ext dep to beacon-nd-array (no cycle)
- expose the batch.rs chunking helpers as pub(crate)
- parity test: provider -> NdSourceExec -> NdBroadcastExec matches the
  v1 broadcast stream at several batch sizes

Build with --workspace (beacon-datafusion-ext needs workspace feature
unification for arrow-schema/serde).
Add any_dataset_as_broadcast_stream in beacon-nd-array: a regular
dataset flows DatasetNdProvider -> NdSourceExec -> NdBroadcastExec
(the broadcast node materializes the un-broadcast nd batches); ragged
datasets stay on the v1 stream. Both file-format openers now call it in
place of the v1 broadcast engine, keeping DataFusion's file machinery
(partitioning, projection pushdown, schema adaptation, caching,
file-stats pruning) intact.

Predicate chunk-pruning is dropped on the regular path for now — the nd
spine has no filter node yet and the scan reports filters as inexact, so
DataFusion's FilterExec preserves correctness. All zarr/netcdf tests
pass.
Introduce a self-describing Arrow encoding for nd arrays: a beacon.nd
extension struct Struct{values: List<T>, dim_sizes: List<UInt32>,
dim_names: List<Utf8>} where one row is one nd array. Because it is an
ordinary Arrow array it can ride through a DataSourceExec as a normal
RecordBatch.

- nd/encoding.rs: encode/decode NdArrowArray + NdRecordBatch, the
  beacon.nd extension field, and logical<->encoded schema helpers.
- NdSourceExec is now a DECODER node: it wraps a child plan producing
  encoded RecordBatches and decodes them to NdRecordBatch in execute_nd
  (schema = decoded/logical). Replaces the NdBatchProvider leaf model;
  NdBatchProvider/MemoryNdBatchProvider removed. Tests drive it from an
  in-memory DataSourceExec of encoded batches.
- re-add NdArrowArray::dims()/values() (the encoder reads them).
- beacon-nd-array: rework the format helper to chunk -> NdRecordBatch ->
  materialize (broadcast) directly; still parity-tested vs v1.

Foundation for wiring formats to emit encoded batches through
DataSourceExec -> NdSourceExec -> NdBroadcastExec as real plan nodes.
The zarr and netcdf file openers now emit beacon.nd-encoded RecordBatches
(one struct row per nd array) instead of flat broadcast batches. Both
create_physical_plan build NdBroadcastExec(NdSourceExec(DataSourceExec))
with the DataSourceExec carrying the encoded (struct) schema, so the nd
data rides up the plan as an ordinary Arrow type, is decoded by
NdSourceExec, and broadcast by NdBroadcastExec — the nd nodes are now
real, EXPLAIN-visible plan nodes.

- encoding.rs: encoded_schema (logical->encoded field wrapping),
  null-struct decode (missing columns -> null scalar), zero-column
  count passthrough, and encode_flat_batch_as_nd (ragged: wrap on a
  synthetic row dim so broadcast is identity).
- beacon-nd-array: any_dataset_as_encoded_stream (regular -> encoded
  chunks; ragged -> v1 flat then row-wrapped + encoded).
- openers adapt in the encoded (struct) domain via the schema adapter;
  null-filled missing columns decode to all-null after broadcast.
- update netcdf opener-level tests to the encoded source schema.

Whole workspace: 867 passed / 0 failed.
Validates the NdBroadcastExec → NdSourceExec → DataSourceExec plan shape
and that rank-0 attributes broadcast to constant columns across the grid,
for both netcdf and zarr.
Adds ScaleOffsetVariableDecoder, wrapping the raw numeric decoder to decode
packed variables to f64 (raw*scale+offset) with the fill value decoded by the
same arithmetic so packed fill cells null out. Mirrors the zarr reader, so
netcdf and zarr now decode scaled variables consistently.
@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.18828% with 69 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.23%. Comparing base (ab1b6ab) to head (d671944).

Files with missing lines Patch % Lines
...con-file-formats/beacon-arrow-netcdf/src/compat.rs 61.70% 18 Missing ⚠️
beacon-datafusion-ext/src/nd/encoding.rs 95.97% 11 Missing ⚠️
beacon-datafusion-ext/src/nd/exec/source_exec.rs 88.15% 9 Missing ⚠️
beacon-datafusion-ext/src/nd/batch.rs 93.39% 7 Missing ⚠️
...e-formats/beacon-nd-array/src/arrow/nd_provider.rs 96.64% 6 Missing ⚠️
...eacon-datafusion-ext/src/nd/exec/broadcast_exec.rs 90.74% 5 Missing ⚠️
...formats/beacon-arrow-zarr/src/datafusion/source.rs 69.23% 4 Missing ⚠️
beacon-datafusion-ext/src/nd/exec/mod.rs 90.00% 3 Missing ⚠️
beacon-datafusion-ext/src/nd/broadcast.rs 98.47% 2 Missing ⚠️
...-formats/beacon-arrow-netcdf/src/datafusion/mod.rs 98.44% 2 Missing ⚠️
... and 2 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #328      +/-   ##
==========================================
+ Coverage   74.51%   75.23%   +0.72%     
==========================================
  Files         290      301      +11     
  Lines       38569    39964    +1395     
==========================================
+ Hits        28738    30067    +1329     
- Misses       9831     9897      +66     
Files with missing lines Coverage Δ
beacon-datafusion-ext/src/nd/array.rs 100.00% <100.00%> (ø)
beacon-datafusion-ext/src/nd/mod.rs 100.00% <100.00%> (ø)
...rmats/beacon-arrow-netcdf/src/datafusion/source.rs 70.93% <100.00%> (+0.96%) ⬆️
...le-formats/beacon-arrow-netcdf/src/decoders/mod.rs 100.00% <ø> (ø)
...s/beacon-arrow-netcdf/src/decoders/scale_offset.rs 100.00% <100.00%> (ø)
...on-file-formats/beacon-nd-array/src/arrow/batch.rs 92.09% <100.00%> (-0.48%) ⬇️
beacon-datafusion-ext/src/nd/dimensions.rs 98.90% <98.90%> (ø)
...le-formats/beacon-arrow-zarr/src/datafusion/mod.rs 79.52% <98.90%> (+5.27%) ⬆️
beacon-datafusion-ext/src/nd/broadcast.rs 98.47% <98.47%> (ø)
...-formats/beacon-arrow-netcdf/src/datafusion/mod.rs 95.81% <98.44%> (+0.48%) ⬆️
... and 8 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@robinskil
robinskil merged commit 88bf786 into main Jul 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant