Skip to content

PR #47 follow-ups: TEP column: 0 divergence + _read_group / _planned_read_group dedup #61

Description

@espg

Summary

Tracking the two open decisions surfaced during PR #47's review folds. Both are non-blockers for the merge of #47 — the PR ships sane defaults — but each warrants a deliberate design call before going further. To be addressed in a follow-up PR once decisions land.


1. column: 0 vs .any(axis=1) semantic divergence (signal_conf_ph TEP filter)

Where it stands. Both shipped ATL03 templates (atl03.yaml, atl03_waveform_counts.yaml) carry {column: 0, op: ne, value: -2} against signal_conf_ph to drop TEP photons. The pre-merge _quality_mask did .any(axis=1) (drop iff -2 across every surface-type column); the post-merge _predicate_mask from #43 requires a column for N-D flags and so cannot express the old reduce-across-all behaviour. The two filters agree exactly when TEP is uniform across columns (which the ATL03 v3 data dictionary asserts), but diverge silently if any granule has signal_conf_ph[i, 0] == -2 with non--2 values in other columns.

Background: original analysis at #47 (comment).

Three options:

  • (A) Keep column: 0 as shipped. Accept the divergence on the v3-dictionary guarantee; tighten the YAML comment to make the choice explicit. Zero code change.
  • (B) Extend _predicate_mask to allow column: null on an N-D flag with a documented .any() reduction semantic. Recovers the prior behaviour exactly; ~5 LOC in src/zagg/processing.py, plus a TestPredicateMask case + a mention in the structured-filter docs. The ATL03 templates would then drop the column: 0 key.
  • (C) Add an explicit reduce: any|all knob to the structured filter that, when set with column: null, applies a configurable reduction across the trailing axis. More general than (B), bigger surface, more docs.

Default position: (A) for now — matches the v3 dictionary and is what's shipped. (B) is the safest if TEP is ever partial-column on a future granule format. (C) is overkill for one use case.


2. Refactor: extract a shared post-read helper from _planned_read_group + _read_group_full

Where it stands. Phase 5's self-review (#47 (review)) flagged that src/zagg/processing.py is now ~1448 lines and _planned_read_group is largely a structural twin of _read_group_full — both follow the same template:

read coords -> spatial mask -> variables / filter datasets -> structured filters
            -> cross-level mask -> expression filters -> pandas / arrow output

The duplication landed because phase 5 was scoped to "wire the planning seam into the read path" without touching the existing flow. CLAUDE.md §4 targets ~1000 lines per module; we're 45 % over.

Proposed shape. A _apply_filters_and_build_output(...) helper takes:

  • lats, lons, arrays_by_path (the just-read data),
  • the data_source config (for coordinates, variables, filters),
  • shard / grid context,
  • optional global_idx + full-length cross_mask for the planned-path case,
  • arrow flag.

Returns the same pandas.DataFrame | pyarrow.Table | None. The planned and full paths shrink to "do my IO + hand off."

Open questions:

  • Where does the cross-level filter logic live — in the helper, or as a pre-computed mask passed in? (Phase 5 puts it on each path; a shared helper would naturally take the precomputed full-length cross_mask + global_idx.)
  • Does this also subsume _read_group_full's [min_idx, max_idx] hyperslice optimization, or stay separate? They're structurally different (single contiguous slice vs. multi-slice list).
  • Worth adding a _read_group_kernel benchmark to confirm the shared helper doesn't regress the full-read path.

Lower risk if landed as its own PR with a parity test against both the planned and full paths' outputs.


Phasing / dependencies

Independent. Either can be addressed first; (1) is smaller and might land first if @espg picks (B) or (C). (2) is the bigger cleanup and benefits from being its own self-contained PR.

Refs

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions