✨ feat(curveframes): frames on a curve that changes in time - #723
Open
nstarman wants to merge 2 commits into
Open
✨ feat(curveframes): frames on a curve that changes in time#723nstarman wants to merge 2 commits into
nstarman wants to merge 2 commits into
Conversation
A builder is called with a single parameter, so a curve needing
`gamma(tau, t)` leaves the time unbound. Until now such a curve constructed
happily and then failed on *every* call with
TypeError: curve2() missing 1 required positional argument: 't'
raised from inside the ODE solve, nowhere near the construction that caused
it. Both the pinned and unpinned cases failed this way, identically.
The guard sits on `AbstractCurveFrameBuilder.__check_init__`, which equinox
runs for every concrete builder, so `BishopBuilder` and
`FrenetSerretBuilder` are covered once rather than each repeating it. The
message names `AtTime(curve, t)`, which already turns a two-argument curve
into a one-argument one and works today.
A pinned `gamma` does not excuse it: `gamma` fixes the station, not the
time, so the curve is still unevaluable. When two-argument routing lands the
guard narrows to the `gamma is None` case; a test pins that it currently
rejects both, so narrowing it early is a visible change rather than a quiet
one.
Arity comes from the existing `_is_two_argument`, so the two idioms GalacticDynamics#712
found misread stay one-argument: `def curve(tau, smoothing=0.1)` and a
`ft.partial`-frozen time. An uninspectable curve raises out of that helper,
matching how `ArcLength` treats one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves coordinaxs.curveframes ergonomics by failing fast when a curve-frame builder is constructed with an unusable two-argument curve, instead of letting a TypeError occur later inside the ODE solve.
Changes:
- Add an
AbstractCurveFrameBuilder.__check_init__guard that rejects curves detected as “two-argument” via the existing_is_two_argumenthelper. - Add unit tests that (1) assert both builders reject two-argument curves at construction, (2) ensure the error message mentions
AtTime, and (3) preserve the two known one-argument “false positive” idioms (defaulted second parameter,functools.partialwith boundt).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/coordinaxs.curveframes/src/coordinaxs/curveframes/_src/base.py | Adds constructor-time validation (__check_init__) to reject two-argument curves early, with a targeted guidance message. |
| packages/coordinaxs.curveframes/tests/unit/test_builder_curve_arity.py | New regression tests covering rejection behavior and guarding known arity-detection edge cases. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+36
to
+42
| _MSG_TWO_ARGUMENT_CURVE = ( | ||
| "curve-frame builders take a one-argument curve `gamma(tau)`, but this " | ||
| "curve requires two positional arguments, `gamma(tau, t)`. A builder is " | ||
| "called with a single parameter, so a two-argument curve leaves the time " | ||
| "unbound and every call would fail inside the solve. Bind the slice first " | ||
| "with `AtTime(curve, t)`, which yields a one-argument curve." | ||
| ) |
A builder is called with a single parameter. For a one-argument curve that is the curve parameter; for a two-argument `gamma(tau, t)` it is now the *time*, with the station supplied as the `gamma` field. `builder(t)` then gives the frame of the time-t slice at that station. A slice at fixed t is a one-argument curve -- exactly what `AtTime` produces -- so the existing machinery applies to it unchanged, parallel-transport ODE included. None of the frame mathematics is time-dependent; only which curve it runs on. That equivalence is the correctness claim, and it holds to 0.000e+00 for position, tangent and rotation alike. The routing lives in `AbstractCurveFrameBuilder._resolve`, which returns a one-argument builder and the parameter to evaluate it at. `__call__`, `location` and `tangent` on the base go through it, as do the two subclass-specific `rotation_matrix` and `tangent` overrides. The four `normal*` accessors need no change: they already read rows of `rotation_matrix`. With no station pinned a two-argument curve still raises at construction -- two unknowns against one call-time slot is arithmetic, not policy -- and the message names both remedies: pin `gamma=`, or bind the slice with `AtTime`. Both slots stay differentiable, checked against the closed form for `gamma(s, t) = (s(1 + t/2), t s^2/10, 0)`: `d(y)/dt = s^2/10` gives 0.1690000000 and `d(y)/ds = t s/5` gives 0.2600000000. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #723 +/- ##
==========================================
+ Coverage 96.50% 96.55% +0.04%
==========================================
Files 265 265
Lines 8735 8799 +64
==========================================
+ Hits 8430 8496 +66
+ Misses 305 303 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lets a frame ride a curve that is itself changing in time.
What it adds
A builder is called with a single parameter. For a one-argument curve that is the curve parameter, unchanged. For a two-argument
gamma(tau, t)it is now the time, with the station along the curve supplied as thegammafield:This is the frames half of time-dependent, arc-length-parametrised TNB. #712 delivered the charts.
Why it is almost no code
builder(t)is the frame of the time-tslice at the pinned station. A slice at fixedtis a one-argument curve — exactly whatAtTimeproduces — so the existing machinery applies to it unchanged, parallel-transport ODE included.No frame mathematics is time-dependent; only which curve it runs on. The routing is one method,
AbstractCurveFrameBuilder._resolve, returning a one-argument builder and the parameter to evaluate it at.That equivalence is the correctness claim, and it is what the tests turn on:
AtTimeslicelocationtangentrotation_matrixon both builders, at three times.
Where the routing had to reach
Patching only the base class was not enough, and the equivalence test is what caught it:
bishop.pyandfrenetserret.pyoverridetangentandrotation_matrix, so a two-argument curve sailed past the base and still died withTypeError: ... missing 1 required positional argument: 't'inside the solve.Routed:
__call__,location,tangenton the base, plus the two subclassrotation_matrixandtangentoverrides. The fournormal*accessors need no change — they already read rows ofrotation_matrix.Both slots stay differentiable
Checked against the closed form for
gamma(s, t) = (s(1 + t/2), t s^2/10, 0)ats = 1.3,t = 1:d(y)/dt = s^2/10d(y)/ds = t s/5The station is a leaf, so it is vmappable and fittable, not just settable.
Still rejected: two-argument with no station
Two unknowns against one call-time slot means no transform can be produced — arithmetic, not policy — so it raises at construction rather than at every call. The message names both remedies: pin
gamma=, or bind the slice withAtTime(curve, t).Before this PR, both configurations constructed happily and then failed on every call from inside the ODE solve, nowhere near the construction that caused it.
Arity detection
Reuses the existing
_is_two_argument, so the two idioms #712 found misread stay one-argument, both with tests:def curve(tau, smoothing=0.1)— a one-argument curve with a tuning knobft.partial(curve, t=...)— a curve whose time the caller already frozeTwo notes for the reviewer
_resolve's annotation carries anoqafor a real tool conflict. It returns the same concrete type it was called on, which is what lets a subclass reach its own helpers.Selfsays that directly and ruff's PYI019 rewrites to it — butbeartyperaisesBeartypeDecorHintPep673Exceptionon PEP 673 in a method whose class it does not decorate, soSelffails at runtime. Hence the explicitTypeVarplus# noqa: PYI019, with the reason recorded at the definition.The equivalence test skips
t = 0deliberately.curve2(s, 0)is the straight line(s, 0, 0), where Frenet--Serret is singular — zero curvature, undefined normal, NaN rotation. That is the apparatus's own degeneracy, andBishopBuilderexists precisely because it does not have it.Verification
Every assertion checked to actually fail, by breaking the implementation:
_resolveswaps the two slotsrm -rf docs/_build && nox -s docs, no warningsprekclean, includingty🤖 Generated with Claude Code