💥 fix(charts): ProlateSpheroidal3D.Delta must be a length - #812
Conversation
The spec has said so since it was written -- *"a required field `Delta`
(focal half-length, an `AbstractQuantity["length"]` with `Delta > 0`)"* --
and nothing enforced it. `Delta=StaticQuantity(2, "s")` built a chart that
declares its components `('area', 'area', 'angle')` and then validated
`s2` data against them without complaint, because `check_data` compares
`mu` to `Delta**2` and never asks what dimension that is.
`__post_init__` now raises `ValueError` naming the dimension it got. The
jaxtyping annotation cannot carry this: nothing enforces it at
construction, and the dimension is not expressible there without narrowing
`Delta` to one quantity type, which would cost the `Quantity` /
`StaticQuantity` choice that makes differentiability opt-in.
Positivity stays in `check_data`, and deliberately: a unit is static and
can be read under `jit`, but `Delta.value` is a tracer, so `> 0` cannot be
branched on at construction. The two rules are enforced in different
places because only one of them can be enforced early.
⚠️ **Breaking**: a `Delta` in any other dimension now raises. Such a chart
was already broken -- its `mu` bound could not be compared against the
`area` its components declare -- so the break surfaces existing bugs
rather than rejecting working code.
The strategies drew `Delta` from its annotation, so they drew seconds as
readily as parsecs; 28 tests constructed such charts. `chart_init_kwargs`
gains a `ProlateSpheroidal3D` overload drawing a length in both quantity
containers, at a modest magnitude -- `Delta` is squared to bound `mu`, and
a draw near the float ceiling squares to infinity.
This also removes a guard from GalacticDynamics#807: `component_domains` no longer needs
to ask whether `Delta**2` is convertible to an area, because a length
squared always is. Only the overflow check remains.
Verification: 11691 passed, 311 skipped, 1 xfailed. `prek run
--all-files` clean; `nox -s docs` succeeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The change aligns implementation with the authoritative spec, adds focused tests (including jit tracing), and updates Hypothesis strategies to avoid generating now-invalid chart instances.
Pull request overview
This pull request enforces the spec-mandated invariant that ProlateSpheroidal3D.Delta is a length (not merely positive/scalar), closing a unit-consistency hole where the chart could declare ("area", "area", "angle") while validating non-area data.
Changes:
- Add a construction-time dimension check in
ProlateSpheroidal3D.__post_init__rejecting non-lengthDelta. - Simplify
component_domains(ProlateSpheroidal3D)by removing the “area-convertibility” guard, leaving only the finite-bound (overflow-to-inf) guard. - Update Hypothesis strategy generation for
ProlateSpheroidal3Dkwargs and add targeted unit/dimension + tracing tests; document the enforcement split in the spec.
File summaries
| File | Description |
|---|---|
src/coordinax/_src/charts/d3.py |
Enforces Delta length dimension at construction via __post_init__, keeping positivity in check_data. |
src/coordinax/_src/charts/register_domains.py |
Removes now-redundant unit-convertibility check; retains overflow/inf guard for domain declaration. |
packages/coordinaxs.hypothesis/src/coordinaxs/hypothesis/charts/_src/chart_kwargs.py |
Adds a chart-specific kwargs strategy override so Hypothesis draws length-valued Delta in either quantity container. |
tests/unit/charts/test_prolate_delta_dimension.py |
Adds tests for accepted length units/containers, rejected non-length units, error messaging, and jax.jit trace behavior. |
tests/unit/charts/test_domain_agreement.py |
Updates the Prolate domain test to cover only the remaining “infinite bound” non-declaration case. |
docs/spec.md |
Documents why dimension is enforced in __post_init__ while positivity remains enforced in check_data. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #812 +/- ##
=======================================
Coverage 96.90% 96.91%
=======================================
Files 272 272
Lines 9414 9433 +19
=======================================
+ Hits 9123 9142 +19
Misses 291 291 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The check as first written cost 21.3us of a 12us chart construction -- `unxt.is_unit_convertible` walks astropy's unit graph on every call. The answer depends only on the unit, and a program uses very few, so an `lru_cache` answers it once per unit at ~0.1us. Measured in one process, same chart, `__post_init__` swapped: no `__post_init__` (main) 10.3us present but empty 11.2us with the memoised check 12.4us So +2.1us on an explicit construction, of which the check is 1.2us. The hot paths pay nothing: `__post_init__` does not re-run when equinox rebuilds the chart from its pytree, which is what `jit` does at every boundary crossing. Pinned by a test, since nothing else would go red if that changed. Comparing dimensions instead is not the cheaper route it looks like -- `dimension_of(unit)` measured ~69us and `dimension_of(quantity)` ~134us, both worse than the call they would replace. Verification: 11693 passed, 311 skipped, 1 xfailed. `prek run --all-files` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The change enforces a spec-stated invariant with JIT-safe design, updates generators accordingly, and adds focused tests covering both failure modes and tracing behavior.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
- Review effort level: Lite
…#815) `upload-coverage-python` does not run for a pull request from a fork: it needs `code-quality: write`, which a fork's token does not carry, so its `if:` gates on `head.repo.full_name == github.repository`. But it is listed unconditionally in the `CI Pass` gate's `needs`, and `alls-green` counts a skipped required job as a failure. So every fork PR went red with all seven real jobs green -- #812 merged that way, and #813 is sitting on the same red now. A permanently failing gate is worse than no gate: it stops carrying information about the runs that do matter. `allowed-skips` excuses the skip and nothing else. The job stays in `needs`, so on a same-repo push, where it does run, a failure still fails the gate. `allowed-failures` would have been the wrong knob -- it would have excused a genuine upload failure too. The other seven required jobs carry no `if:`, so this is the only one that can skip. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The spec has said so since it was written:
Nothing enforced it.
check_datacomparesmuagainstDelta**2and never asks what dimension that is, so:A chart that declares
areaand validatess2. #807 worked around it —component_domainsdeclares nothing for aDeltait cannot use — but the hole is in the chart, so this closes it there.Where each rule is enforced, and why they differ
__post_init__raisesValueErrornaming the dimension it got:Positivity stays in
check_data, deliberately. A unit is static and readable underjit;Delta.valueis a tracer, so> 0cannot be branched on at construction. The chart is built inside traced code by everypt_mapthat takes one as an argument, so the dimension check had to be tracer-safe — it is, and there's a test pinning that.The jaxtyping annotation cannot carry this either. It is not enforced at construction (a non-scalar
Deltais accepted today despiteReal[..., ""]), and the dimension is not expressible there without narrowingDeltato a single quantity type — which would cost theQuantity/StaticQuantitychoice that makes differentiability opt-in per instance.A
Deltain any other dimension now raises at construction. Such a chart was already broken — itsmubound could not be meaningfully compared against theareaits own components declare — so this surfaces existing bugs rather than rejecting working code. No in-repo caller passed a non-lengthDelta.Fallout
The strategies were the real users of the looseness.
chart_init_kwargsbuilds parameters from each field's annotation, andDelta's pins no dimension, so it drew seconds as readily as parsecs — 28 tests constructed such charts. There is already aplumdispatch seam for per-chart overrides; this adds the first one, drawing a length in both quantity containers (which one is used is a real distinction here:StaticQuantitycontributes no pytree leaves,Quantitycontributes one). Magnitude is kept modest and its bounds are powers of two, sinceDeltais squared to boundmuandfloats(width=32)requires exactly-representable bounds.It also deletes a guard from #807.
component_domainsno longer asks whetherDelta**2is convertible to an area — a length squared always is. Only the overflow check remains, for aDeltanear the float ceiling.Verification
11691 passed, 311 skipped, 1 xfailed.
prek run --all-filesclean;nox -s docssucceeds. New tests cover both containers across three length units, four rejected dimensions, the error naming the dimension, and that a dynamicDeltastill traces.🤖 Generated with Claude Code