Skip to content

💥 fix(charts): ProlateSpheroidal3D.Delta must be a length - #812

Merged
nstarman merged 2 commits into
GalacticDynamics:mainfrom
nstarman:claude/prolate-delta-is-a-length
Sep 4, 2026
Merged

💥 fix(charts): ProlateSpheroidal3D.Delta must be a length#812
nstarman merged 2 commits into
GalacticDynamics:mainfrom
nstarman:claude/prolate-delta-is-a-length

Conversation

@nstarman

@nstarman nstarman commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

The spec has said so since it was written:

Carries a required field Delta (focal half-length, an AbstractQuantity["length"] with Delta > 0).

Nothing enforced it. check_data compares mu against Delta**2 and never asks what dimension that is, so:

>>> chart = cxc.ProlateSpheroidal3D(Delta=u.StaticQuantity(2.0, "s"))
>>> chart.coord_dimensions
('area', 'area', 'angle')
>>> chart.check_data({"mu": u.Q(9.0, "s2"), ...}, values=True)   # accepted

A chart that declares area and validates s2. #807 worked around it — component_domains declares nothing for a Delta it 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__ raises ValueError naming the dimension it got:

ProlateSpheroidal3D.Delta is the focal length and must have dimensions of
length, got s (time).

Positivity stays in check_data, deliberately. A unit is static and readable under jit; Delta.value is a tracer, so > 0 cannot be branched on at construction. The chart is built inside traced code by every pt_map that 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 Delta is accepted today despite Real[..., ""]), and the dimension is not expressible there without narrowing Delta to a single quantity type — which would cost the Quantity/StaticQuantity choice that makes differentiability opt-in per instance.

⚠️ Breaking

A Delta in any other dimension now raises at construction. Such a chart was already broken — its mu bound could not be meaningfully compared against the area its own components declare — so this surfaces existing bugs rather than rejecting working code. No in-repo caller passed a non-length Delta.

Fallout

The strategies were the real users of the looseness. chart_init_kwargs builds parameters from each field's annotation, and Delta's pins no dimension, so it drew seconds as readily as parsecs — 28 tests constructed such charts. There is already a plum dispatch 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: StaticQuantity contributes no pytree leaves, Quantity contributes one). Magnitude is kept modest and its bounds are powers of two, since Delta is squared to bound mu and floats(width=32) requires exactly-representable bounds.

It also deletes a guard from #807. component_domains no longer asks whether Delta**2 is convertible to an area — a length squared always is. Only the overflow check remains, for a Delta near the float ceiling.

Verification

11691 passed, 311 skipped, 1 xfailed. prek run --all-files clean; nox -s docs succeeds. New tests cover both containers across three length units, four rejected dimensions, the error naming the dimension, and that a dynamic Delta still traces.

🤖 Generated with Claude Code

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>
Copilot AI lite review requested due to automatic review settings September 4, 2026 08:37
@github-actions github-actions Bot added 📝 Add / update documentation Add or update documentation. ✅ Add / update / pass tests Add, update, or pass tests. 💥 Introduce breaking changes Introduce breaking changes. labels Sep 4, 2026

Copilot AI 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.

🟢 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-length Delta.
  • Simplify component_domains(ProlateSpheroidal3D) by removing the “area-convertibility” guard, leaving only the finite-bound (overflow-to-inf) guard.
  • Update Hypothesis strategy generation for ProlateSpheroidal3D kwargs 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

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.91%. Comparing base (d984f00) to head (69fec27).
⚠️ Report is 3 commits behind head on main.

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.
📢 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.

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>
@github-actions github-actions Bot added the ⚡️ Improve performance Improve performance. label Sep 4, 2026
@nstarman
nstarman requested a lite review from Copilot September 4, 2026 10:05
@nstarman
nstarman merged commit 66d7489 into GalacticDynamics:main Sep 4, 2026
19 of 21 checks passed
@nstarman
nstarman deleted the claude/prolate-delta-is-a-length branch September 4, 2026 10:06

Copilot AI 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.

🟢 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

nstarman added a commit that referenced this pull request Sep 4, 2026
…#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📝 Add / update documentation Add or update documentation. ✅ Add / update / pass tests Add, update, or pass tests. ⚡️ Improve performance Improve performance. 💥 Introduce breaking changes Introduce breaking changes.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants