Skip to content

Store coordinate envelopes in original units; a bare range means native units - #872

Merged
d-chambers merged 8 commits into
devfrom
select-native-units
Aug 12, 2026
Merged

Store coordinate envelopes in original units; a bare range means native units#872
d-chambers merged 8 commits into
devfrom
select-native-units

Conversation

@d-chambers

@d-chambers d-chambers commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

Closes #863.

The unreleased index rewrite generalized indexing from time alone to every coordinate, and converted each numeric envelope to base SI on the way in while storing the base unit in a column documented as holding the original. So get_contents() described envelopes no patch it yields actually has — a degrees coordinate read as radians, x_min = -2.042 for a patch whose x is -117.0 — and a bare numeric range meant SI at the spool while the same range on the patch meant the coordinate's own units, so spool.select(x=(-117.0, -116.9)) silently returned nothing where patch.select kept every channel.

Both are confined to the unreleased window. On the last release the index describes only time, so a range on any other dimension fell through to the patch at load and read natively, and there were no distance columns to misreport. This restores the released meaning rather than choosing a new one.

Envelopes are now stored in each coordinate's original units beside the original unit string:

  • A bare numeric range keeps the meaning it has always had — the coordinate's own units, identical to Patch.select — for selection, chunk lengths, and residual trims alike.
  • get_contents() shows native values beside new public {name}_units columns, so distance_min = 65.6 is self-explaining.
  • Quantity selectors and quantity chunk lengths mean one physical interval. A quantity query converts itself once per distinct compatible stored unit into OR branches inside the existing coord_defs semi-join, so candidacy stays one indexed scan rather than a per-unit fan-out. A quantity length is measured as a length, so 20 °C of extent is 36 °F.
  • The chunk planner normalizes compatible spellings to one unit per dimensionality before partitioning, so metres still plan and merge with feet, and every output patch comes back in the unit its row advertises.

In an archive whose files state different units for one coordinate, a bare range selects a per-file native interval, sorting orders native magnitudes, and a relative offset measures against a pooled span. That is the accepted consequence of matching per-patch semantics: a spool has no single unit to read a bare number in. Archives whose files agree on a unit are unaffected.

This lands before the inventory's phase 3 (#857), whose inventory-backed select builds on this machinery — geometry axes arrive in the CRS's own units, exactly the case the SI reading got wrong.

The changelog entry from #855 describing a chunk fix for the same unreleased regression is dropped: its net effect for users is zero and its closing clause is no longer true.

Filed while verifying, and deliberately not fixed here because both reproduce unchanged on dev: #870 (chunk lengths that are not a multiple of the sample step drop one sample per boundary) and #871 (chunk → select → re-chunk over-includes and duplicates samples).

Changelog

  • added: the index records every coordinate's envelope in that coordinate's own units, and get_contents() shows them beside new {name}_units columns, so a spool reports distance and every other coordinate rather than only time (#863).
  • changed: a bare numeric range means the coordinate's own units and a quantity means one physical interval, so chunk(distance=100) on a feet coordinate is 100 feet while 100 * dc.units.m converts per file.
  • changed: a unit-bearing chunk length raises when the spool records no units for that coordinate, and is measured as a length rather than converted as a temperature (20 °C of extent is 36 °F, never 68).

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes.
  • documented the new feature with docstrings and/or appropriate doc page.
  • included tests. See testing guidelines.
  • added the "ready_for_review" tag once the PR is ready to be reviewed.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@d-chambers, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a0143ae5-fa90-40b2-a400-2547adbab7c1

📥 Commits

Reviewing files that changed from the base of the PR and between 09a9ccb and 6ecd117.

📒 Files selected for processing (7)
  • dascore/io/index/planned.py
  • dascore/io/index/query.py
  • dascore/utils/chunk_plan.py
  • docs/changelog.qmd
  • tests/test_core/test_spool_select_spec.py
  • tests/test_io/test_index/test_planned.py
  • tests/test_utils/test_chunk.py

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e3bb401-b461-49f2-985f-d013d643f749

📥 Commits

Reviewing files that changed from the base of the PR and between b972661 and 09a9ccb.

📒 Files selected for processing (1)
  • docs/changelog.qmd

📝 Walkthrough

Walkthrough

The change preserves original coordinate units in index envelopes and dataframe results. Bare ranges use native units, while quantity ranges convert during query or assembly. Chunk planning normalizes compatible units by dimensionality. The index schema version changes to 8.

Changes

Native coordinate units

Layer / File(s) Summary
Range and index unit semantics
dascore/utils/misc.py, dascore/io/index/ingest.py, dascore/io/index/query.py, dascore/io/index/catalog.py, dascore/io/index/schema.py, tests/test_io/test_index/*
Canonical ranges retain per-bound units. Coordinate envelopes remain in original units. Bare ranges use native units. Quantity ranges convert against stored units during selection.
Selection and result presentation
dascore/io/index/planned.py, dascore/io/index/backend.py, dascore/io/index/indexer.py, dascore/core/spool.py, dascore/utils/pd.py, tests/test_core/test_spool_select_spec.py, docs/notes/spool_index.qmd, docs/notes/spool_selection.qmd
Planned catalogs preserve source unit spellings and apply unit-aware residual trimming. Private unit columns are exposed safely as public columns. Selection and spool result tests cover native and mixed-unit behavior.
Chunk planning and patch assembly
dascore/utils/chunk_plan.py, dascore/utils/patch_assembly.py, tests/test_core/test_patch_chunk.py, tests/test_utils/test_chunk.py, docs/notes/spool_chunking.qmd, docs/changelog.qmd
Compatible chunk units are normalized by dimensionality. Quantity lengths use coordinate units and affine-safe conversion. Patch assembly applies plan-unit trims and converts compatible members before merging.

Possibly related PRs

Suggested labels: bug, documentation, IO, patch, spool

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #863 by aligning Spool.select with Patch.select, preserving native envelopes, and retaining quantity-based physical selection.
Out of Scope Changes check ✅ Passed The code, documentation, changelog, and tests support the linked issue and stated objectives; issues #870 and #871 are explicitly excluded and unchanged.
Docstring Coverage ✅ Passed Docstring coverage is 98.84% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly states the main changes: preserving coordinate envelopes in original units and interpreting bare ranges in native units.
Description check ✅ Passed The description explains the problem, solution, scope, linked issues, documentation, tests, and checklist items in sufficient detail.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch select-native-units

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added bug Something isn't working documentation Improvements or additions to documentation IO Work for reading/writing different formats patch related to Patch class spool related to Spool class labels Aug 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dascore/io/index/planned.py`:
- Around line 402-411: Update the unit comparison in the surrounding trim-hint
logic to treat null or NaN plan units as missing, matching the existing
_in_plan_units behavior. Use the established pandas null check before comparing
plan_units and source_units, while preserving hint removal when both units are
present but differ.

In `@dascore/utils/chunk_plan.py`:
- Around line 286-311: Update _normalize_chunk_units around the envelope
conversions using to_numpy(dtype=float) so datetime64 and timedelta64 values are
not passed to numeric unit conversion. Restrict this normalization path to
numeric envelopes, or convert time-like rows through their stored nanosecond
fields while preserving canonical "s" units and the existing mixed-unit
normalization behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ebbb55d6-caa7-4e70-854d-3fed45e5f4eb

📥 Commits

Reviewing files that changed from the base of the PR and between 541b25a and 78ad481.

📒 Files selected for processing (23)
  • dascore/core/spool.py
  • dascore/io/index/backend.py
  • dascore/io/index/catalog.py
  • dascore/io/index/indexer.py
  • dascore/io/index/ingest.py
  • dascore/io/index/planned.py
  • dascore/io/index/query.py
  • dascore/io/index/schema.py
  • dascore/utils/chunk_plan.py
  • dascore/utils/misc.py
  • dascore/utils/patch_assembly.py
  • dascore/utils/pd.py
  • docs/changelog.qmd
  • docs/notes/spool_chunking.qmd
  • docs/notes/spool_index.qmd
  • docs/notes/spool_selection.qmd
  • tests/test_core/test_patch_chunk.py
  • tests/test_core/test_spool_select_spec.py
  • tests/test_io/test_index/test_catalog.py
  • tests/test_io/test_index/test_heterogeneity_stress.py
  • tests/test_io/test_index/test_index_contract.py
  • tests/test_io/test_index/test_index_edge_cases.py
  • tests/test_utils/test_chunk.py

Comment thread dascore/io/index/planned.py
Comment thread dascore/utils/chunk_plan.py
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (0384375) to head (6ecd117).

Additional details and impacted files
@@            Coverage Diff             @@
##               dev      #872    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files          177       177            
  Lines        20017     20153   +136     
==========================================
+ Hits         20017     20153   +136     
Flag Coverage Δ
network 46.74% <14.15%> (-0.27%) ⬇️
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78ad481023

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +383 to +385
min_num=float(summary.min),
max_num=float(summary.max),
step_num=None if pd.isnull(step) else float(step),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize envelopes before resolving relative ranges

When a view contains compatible coordinates expressed in different units, storing these raw native magnitudes makes PatchCatalog._relative_to_absolute() compute its global minimum and maximum from incomparable values. For example, identical spans represented as 0–300 m and 0–984 ft produce a global maximum of 984, after which the resulting bare bounds are interpreted natively by both patches; relative=True therefore trims different physical intervals and can leave one patch effectively untrimmed. Convert the envelopes to a common unit, or resolve the offsets per unit group, before calculating the global range.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and left as-is for the same reason as the sort comment — with a note added, since this one is a global computation rather than per-file. Verified: across 0-299 m and 0-981 ft the global envelope becomes 0-981, so a relative offset trims different physical intervals and can leave the metre patch effectively untrimmed. That is the mixed-spelling archive again: 'ten percent of the spool's span' has no single meaning when the members do not share a unit, exactly as a bare bound does not. Converting for this one path would mean picking a canonical unit again, which is what the PR removes. Worth revisiting if mixed-unit archives turn out to be common in practice — for a unit-homogeneous archive, the normal case, relative offsets are unchanged.

Comment thread dascore/io/index/query.py
Comment on lines +392 to +397
if typed.units is None:
val = bound
else:
val = convert_units(
bound, to_units=unit, from_units=typed.units
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer mixed-bound ordering checks until unit conversion

For a range containing one bare bound and one quantity bound, _range_bounds() compares their unconverted magnitudes before this per-unit branch runs. Thus a feet coordinate queried with distance=(100, 50 * m) raises lo > hi because it compares 100 to 50, even though the converted interval is valid (100–164 ft). Mixed ranges are explicitly handled here, so their ordering must be validated only after each bound has been expressed in the stored coordinate unit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f0ab669. Confirmed the divergence: spool.select(distance=(100, 50 * m)) on a feet coordinate raised lo > hi while patch.select returned 101.71-164.04 ft — exactly the class of Patch/Spool mismatch this PR exists to remove. The ordering check now runs only when both bounds share a frame of reference; a mixed range is validated per stored unit in build_coord_clause, where each bound is expressed against the definition being tested. Genuinely reversed ranges, bare or quantity, still raise.

Comment on lines +383 to +384
min_num=float(summary.min),
max_num=float(summary.max),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Convert native minima before coordinate sorting

When patches express the sorted coordinate in compatible but different units, Spool.sort() still orders directly by the stored numeric minimum in _order_clause, but these values are now native magnitudes. For example, a patch starting at 2 m is placed before one starting at 3 ft because SQL compares 2 with 3, although 3 ft is physically smaller; the previous canonical storage ordered this correctly. Coordinate sort keys need conversion to a common unit before comparison.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed but intended, so not changed. Ordering a mixed-unit archive by native magnitudes is the accepted consequence of a bare number meaning the coordinate's own units — the same rule that makes a bare range select a per-file interval there. A spool spanning several spellings has no single unit to sort in, and reintroducing a canonical one for the sort key alone would restore precisely the patch/spool divergence this PR removes. It is documented in the changelog and in the selection note; pass a quantity when one physical ordering is what you want. Unit-homogeneous archives, the normal case, are unaffected.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

✅ Documentation built:
👉 Download
Note: You must be logged in to github and a DASDAE member to access the link.

… units

The index recorded numeric coordinate envelopes converted to base SI
beside a units column holding the base unit, so a degrees coordinate
showed radians and a bare spool selector meant SI while the same range
on the patch meant the coordinate's own units (#863). Envelopes now
store the original magnitudes beside the original unit string — the
index shows what the patch shows — and a bare numeric range means
native units everywhere: select, chunk lengths, and the residual trim,
which passes bare ranges straight through to Patch.select. Quantity
queries convert themselves once per distinct compatible stored unit
into OR branches inside the existing coord_defs semi-join, and the
chunk planner re-spells compatible unit families to one unit per
dimensionality before partitioning, so metres still merge with feet.
Assembly hands trims down as quantities in the plan's unit. The index
version is bumped; a quantity chunk length on a frame recording no
units now raises instead of silently assuming SI.
get_contents (and the indexer's flat relation) now expose each private
_{name}_units column under its public name, so a native envelope value
sits beside the unit that scales it; the private spelling remains for
the planners, whose merge policing compares public columns. The
presented-envelope adjustment converts a quantity range per distinct
row unit instead of applying SI magnitudes to native columns. The
bare_is_si machinery is gone: bare ranges pass through everywhere, and
only unit-bearing queries canonicalize.
Codex and two adversarial agents, five confirmed defects:

- A bare bound beside a quantity bound was reinterpreted in the
  quantity's unit at load; _CanonicalRange now carries units per bound,
  so bare bounds stay native exactly as Patch.select reads them.
- Re-planning a quantity-selected derived view applied SI magnitudes to
  native envelopes; collapse_working_df now converts per row unit
  through the shared magnitudes_in kernel.
- The coord-def dedup key was a physically-simplified fingerprint, so
  one coordinate spelled in metres and feet collided and the first
  spelling's metadata lied for the second (false select negatives,
  mislabeled unions). The stored key now carries the unit spelling;
  fingerprints stay semantic.
- Plan trims are magnitudes in the partition's normalized unit, but
  members from differently-spelled sources read them natively (empty
  patches, wrong intervals). Members now carry the plan unit, trims
  convert as quantities at assembly, and a bare read hint is dropped
  when the source spells the coordinate differently.
- Affine quantity chunk lengths converted absolutely — 20 degC of
  extent became 68 degF; lengths now convert as deltas, which also
  accepts scaled unit spellings pint's .to() rejects.

An attr named like a coordinate's units column is omitted from the flat
view with a warning (coord wins, same rule as envelope collisions), the
query layer treats "" units as unset, and the remaining SI-era comments
and tests now state the native-units contract.
The Codex verification round found three defects this branch had
introduced:

- A chunk output drawing on a single member never converted to the
  partition's unit, because only merging visits that conversion. A feet
  member was therefore published under a row claiming metres, so
  get_contents described an envelope no patch it yields actually had and
  a bare select on that envelope matched nothing. Members are now
  re-expressed in the plan's unit as they load (identity mode, which
  promises the untouched patch, is exempt), which also makes the
  merge-time normalizer a no-op rather than the only guard.
- The column recording a member's own unit spelling could be a real
  coordinate's: a coordinate named "{dim}_source" owns
  "_{dim}_source_units" outright. It is now "_{dim}_units_source", which
  no coordinate unit column can spell.
- Re-chunking a derived view renamed that column a second time, leaving
  two columns of one name; one silently vanished when the rows became
  load kwargs, so a member could be trimmed in the wrong unit.

The units presentation helper documents why it never overwrites a public
column, and the merge-time normalizer's conversion branch is now tested
directly since assembly normalizes before it runs.
get_quantity is nullable, so the type checker rejected reading
to_base_units off it. Null and empty spellings are filtered out a few
lines above, so the assert states what the filter already guarantees.
Checked what the released version actually does: its index describes
only time, so a coordinate range on any other dimension was never
evaluated by the index at all — it fell through to the patch at load,
which reads it in the coordinate's own units, and get_contents showed no
distance columns to misreport. Bare ranges have therefore always meant
native units; the canonical-SI storage arrived with the unreleased index
rewrite and never shipped.

So this entry no longer claims to change what a bare range means. It
describes what a reader upgrading from the last release actually gets:
coordinates other than time indexed at all, their envelopes and units
reported truthfully, and quantities as the way to mean one physical
interval. The entry describing a chunk fix for the same unreleased
regression is dropped — its net effect for users is zero and its closing
clause is no longer true — and the raise-without-units and delta-length
rules move to the bullet for the quantity chunk feature they belong to.
- A range mixing a bare bound with a unit-bearing one held magnitudes in
  two frames of reference, and the ordering check compared them raw:
  distance=(100, 50 * m) on a feet coordinate was rejected as lo > hi
  though the patch accepts it as 100-164 ft. The check now runs only
  when both bounds share a frame; the per-unit branch converts each
  bound against the definition it tests. Bounds that really are reversed
  still raise.
- Unit normalization is restricted to numeric envelopes. Time-like ones
  are canonical nanoseconds whatever unit the coordinate names, so there
  is nothing to re-spell, and converting them as floats raised.
- A row states no unit as NaN rather than None, and NaN never equals
  itself, so every member of a unitless partition looked like a unit
  mismatch and lost its read hint. One helper now reads an absent unit
  the same way everywhere it is consulted.
@d-chambers
d-chambers force-pushed the select-native-units branch from 09a9ccb to f0ab669 Compare August 12, 2026 04:58
@d-chambers d-chambers added the ready_for_review PR is ready for review label Aug 12, 2026
A relative bound measures against a span pooled from the members'
magnitudes, so an archive whose files disagree on a unit gets the same
treatment there as it does for bare ranges and sorting.
@d-chambers
d-chambers merged commit 32dd999 into dev Aug 12, 2026
28 checks passed
@d-chambers
d-chambers deleted the select-native-units branch August 12, 2026 07:15
@d-chambers d-chambers removed the ready_for_review PR is ready for review label Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation IO Work for reading/writing different formats patch related to Patch class spool related to Spool class

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant