Skip to content

Handle pint quantities explicitly in to_float - #834

Merged
d-chambers merged 2 commits into
devfrom
to-float-quantity
Aug 7, 2026
Merged

Handle pint quantities explicitly in to_float#834
d-chambers merged 2 commits into
devfrom
to-float-quantity

Conversation

@d-chambers

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

Copy link
Copy Markdown
Contributor

Description

Follow-up to #833, split out so the index schema bump stays reviewable on its own.

to_float handled pint quantities backwards: it rejected everything with dimensions — including the seconds quantity its own docstring implies should work — and silently accepted everything without.

input before after
2 * s DimensionalityError 2.0
2 * min DimensionalityError 120.0
[1., 2.] * min DimensionalityError array([60., 120.])
25 * MB 200000000.0 UnitError
50% 0.5 UnitError
10 * m DimensionalityError UnitError

The silent half is the dangerous one. to_float's fallback is float(obj), and pint's Quantity.__float__ converts a dimensionless quantity to base units. Bytes are dimensionless in pint (byte = 8 * bit, bit dimensionless), so a data size came back as its count in bits — eight times too large, with no exception and a plausible-looking number. It was found while adding size-based chunking, where an 8× overestimate of a memory budget is exactly the failure the feature exists to prevent.

Why reject rather than pick a conversion

Two alternatives were considered and rejected:

  • Strip units (.magnitude) is unit-blind: to_float(10 * km) and to_float(10 * m) would both give 10.0, while callers treat the result as a number in canonical units (proc/filter.py compares it against Nyquist). It also makes 50%50.0, contradicting the percent-is-a-fraction convention in maybe_convert_percent_to_fraction and coords._get_compatible_value.
  • Convert to base units is today's dimensionless behavior generalized — and bytes' base unit is the bit, so the 8× trap survives.

Instead, treat time as the function's domain, which is what every existing overload already does (datetime64 → seconds since epoch, timedelta64 → seconds, plain numbers passed through). A metre or a megabyte has no float representation in that domain, so it raises UnitError naming convert_units and get_byte_count as the explicit alternatives.

The magnitude is recursed back through to_float rather than returned directly, which is what makes array-valued quantities take the array path and an integer magnitude still widen to float.

Is anything relying on the old behavior?

No. The full suite was run with to_float instrumented to record every call receiving a Quantity: exactly one hit across 8382 tests, and it was the test asserting the bits behavior itself. The only semantic loss is to_float(get_quantity("50%")) == 0.5, which nothing used and which is far more likely a bug than an intent when it reaches a seconds-converter.

Drive-by

patch.notch_filter(distance=5 * dc.units.m) on a coordinate with no units leaked pint's DimensionalityError through proc/filter.py. It now raises UnitError: Cannot filter 'distance' with 5 m: the coordinate has no units.... The generic to_float message would have been misleading in a distance filter, hence the specific one.

Changelog

  • changed breaking: to_float converts a time quantity to seconds and raises UnitError for every other quantity, instead of silently reducing a dimensionless one to base units (25 MB came back as 2e8, the count in bits); use convert_units or get_byte_count for an explicit conversion.
  • fixed breaking: filtering a coordinate that carries no units with any quantity raises UnitError; a dimensionless one was previously read as a bare number (20 % became 0.2 Hz), so a call that silently did the wrong thing now stops.

Checklist

I have (if applicable):

Summary by CodeRabbit

  • New Features

    • Added support for converting time quantities to seconds when using numeric time utilities.
    • Added clearer handling for unit-bearing values during filtering.
  • Bug Fixes

    • Invalid unit combinations now raise a clear UnitError instead of proceeding silently or exposing a lower-level conversion error.
    • Non-time quantities, such as data sizes, are no longer implicitly converted to numeric values.
  • Documentation

    • Updated the unreleased changelog with the new quantity-conversion and filtering behavior.

@d-chambers d-chambers added the ready_for_review PR is ready for review label Aug 7, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Aug 7, 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: 14 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: 3506ac56-ce77-456b-a2ee-213f8bbd5ddd

📥 Commits

Reviewing files that changed from the base of the PR and between 6dfc283 and 4b688e8.

📒 Files selected for processing (4)
  • dascore/proc/filter.py
  • dascore/utils/time.py
  • docs/changelog.qmd
  • tests/test_proc/test_filter.py
📝 Walkthrough

Walkthrough

The PR adds Pint quantity handling to to_float, converts time quantities to seconds, rejects unsupported dimensions with UnitError, and validates quantity-valued notch-filter inputs against unitless coordinates.

Changes

Quantity and unit handling

Layer / File(s) Summary
Time quantity conversion
dascore/utils/time.py, tests/test_utils/test_time.py, docs/changelog.qmd
to_float converts scalar and array time quantities to seconds. Unsupported and data-size quantities raise UnitError. Tests and the changelog document the behavior.
Notch filter unit validation
dascore/proc/filter.py, tests/test_proc/test_filter.py
notch_filter raises UnitError when a quantity-valued filter input targets a coordinate without units. The regression test checks the error message.

Suggested labels: bug, proc, transform

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: explicit handling of Pint quantities in to_float.
Description check ✅ Passed The description explains the problem, solution, alternatives, related filter behavior, documentation, tests, and checklist status.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch to-float-quantity
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch to-float-quantity

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 proc Related to processing module transform Related to transform operations labels Aug 7, 2026
@d-chambers

Copy link
Copy Markdown
Contributor Author

Update after adversarial review

Reviewed alongside #833; two findings addressed in 9366ad37. Full suite 8323 passed.

The notch-filter guard is broader than the changelog said, and its message was wrong. It fires on any quantity when the coordinate has no units, including dimensionless ones — but the message claimed the value was "unit-bearing", which is false for 0.2 dimensionless. The behavior is right and worth keeping: before this PR a dimensionless quantity was silently read as a bare number, so 20 % became 0.2 Hz. The message now says a quantity cannot be interpreted against a unitless coordinate, the test is parametrized over metres / percent / dimensionless, and the changelog describes the real scope.

The quantity contract was invisible in the rendered API docs. It lived only in the private _quantity_to_float docstring, which the renderer does not surface; it is now on to_float's public docstring.

Verified clean

A reviewer instrumented to_float to record every pint.Quantity reaching it while emulating pre-PR semantics, then ran the full suite including the generated doc-code tests: every hit originated in this PR's own new tests, none in dascore/ and none in any docs/**/*.qmd block. All 44 to_float call sites were also read individually — units.py:146, utils/patch.py:1033, io/xml_binary/utils.py:242, coords.py's _get_compatible_value and tolerance handling, and proc/filter.py all strip units or return bare magnitudes before to_float sees them. Recursion termination was probed across 21 constructions (0-d, empty, masked, nested, timedelta64 and datetime64 magnitudes, foreign registries) with no unbounded recursion, and .to("s") was confirmed to raise only DimensionalityError — offset, log, angle and count units all produce a clean UnitError.

Known, not fixed here

to_float(Quantity(np.array(2.0), "s")) raises TypeError: len() of unsized object. The cause is pre-existing in _array_to_float, which has the same hole for a bare 0-D array (to_float(np.array(2.0)) fails on dev too), so #802 evidently did not cover to_float. It is hard to reach — pint downcasts 0-D magnitudes to np.float64 on any arithmetic, so every natural spelling works — and fixing _array_to_float is a separate concern from this PR. Flagging rather than folding it in.

to_float rejected every dimensional quantity -- including the seconds
quantity its own docstring implies should work -- while silently
accepting every dimensionless one. The silent path is the dangerous
half: pint's __float__ converts a dimensionless quantity to base units,
and information's base unit is the bit, so to_float(25 * MB) returned
200000000.0 rather than raising.

Register Quantity so a time quantity converts to its duration in
seconds and everything else raises UnitError naming the explicit
alternatives. This matches the function's actual domain: every existing
overload converts datetime64 to seconds since epoch, timedelta64 to
seconds, or passes a plain number through. The magnitude is recursed
back through to_float so an array-valued quantity takes the array path
and an integer magnitude still widens to float.

Instrumenting the full test suite found exactly one Quantity reaching
to_float, in a test asserting the bits behavior, so nothing depended on
the old semantics.

Also raise UnitError with an explanatory message when filtering a
unitless coordinate with a unit-bearing value, rather than letting
pint's DimensionalityError leak out of proc.filter.
Review found the guard's message claimed the value was "unit-bearing",
which is wrong for a dimensionless quantity. Every quantity is rejected,
including dimensionless ones: there is nothing to convert against, and
the previous behavior read `20 %` as 0.2 Hz. Say that, test all three
kinds, and describe the real scope in the changelog.

Also surface the quantity contract on to_float's public docstring; the
overload's own docstring is not rendered in the API docs.
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (23b575a) to head (4b688e8).

Additional details and impacted files
@@            Coverage Diff            @@
##               dev      #834   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          164       164           
  Lines        17924     17936   +12     
=========================================
+ Hits         17924     17936   +12     
Flag Coverage Δ
network 48.48% <35.71%> (-0.02%) ⬇️
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.

@d-chambers
d-chambers merged commit 99b425f into dev Aug 7, 2026
27 checks passed
@d-chambers
d-chambers deleted the to-float-quantity branch August 7, 2026 12:31
@d-chambers d-chambers removed the ready_for_review PR is ready for review label Aug 11, 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 proc Related to processing module transform Related to transform operations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant