Skip to content

✨ feat(curveframes): frames on a curve that changes in time - #723

Open
nstarman wants to merge 2 commits into
GalacticDynamics:mainfrom
nstarman:claude/twoarg-guard
Open

✨ feat(curveframes): frames on a curve that changes in time#723
nstarman wants to merge 2 commits into
GalacticDynamics:mainfrom
nstarman:claude/twoarg-guard

Conversation

@nstarman

@nstarman nstarman commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

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 the gamma field:

b = cxfc.BishopBuilder(curve2, "km", gamma=u.Q(1.3, "km"))
b.location(u.Q(1.7, "s"))     # the frame at that station, on the t = 1.7 slice

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-t slice at the pinned 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.

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:

routed vs hand-built AtTime slice
location max abs diff 0.000e+00
tangent max abs diff 0.000e+00
rotation_matrix max abs diff 0.000e+00

on 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.py and frenetserret.py override tangent and rotation_matrix, so a two-argument curve sailed past the base and still died with TypeError: ... missing 1 required positional argument: 't' inside the solve.

Routed: __call__, location, tangent on the base, plus the two subclass rotation_matrix and tangent overrides. The four normal* accessors need no change — they already read rows of rotation_matrix.

Both slots stay differentiable

Checked against the closed form for gamma(s, t) = (s(1 + t/2), t s^2/10, 0) at s = 1.3, t = 1:

expected got
d(y)/dt = s^2/10 0.169 0.1690000000
d(y)/ds = t s/5 0.26 0.2600000000

The 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 with AtTime(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 knob
  • ft.partial(curve, t=...) — a curve whose time the caller already froze

Two notes for the reviewer

_resolve's annotation carries a noqa for a real tool conflict. It returns the same concrete type it was called on, which is what lets a subclass reach its own helpers. Self says that directly and ruff's PYI019 rewrites to it — but beartype raises BeartypeDecorHintPep673Exception on PEP 673 in a method whose class it does not decorate, so Self fails at runtime. Hence the explicit TypeVar plus # noqa: PYI019, with the reason recorded at the definition.

The equivalence test skips t = 0 deliberately. 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, and BishopBuilder exists precisely because it does not have it.

Verification

Every assertion checked to actually fail, by breaking the implementation:

mutation result
_resolve swaps the two slots 4 tests fail
guard neutered the station-less rejection tests fail
  • 918 curveframes tests pass
  • clean rm -rf docs/_build && nox -s docs, no warnings
  • prek clean, including ty

🤖 Generated with Claude Code

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>
Copilot AI lite review requested due to automatic review settings August 15, 2026 08:32

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.

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_argument helper.
  • 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.partial with bound t).

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."
)
@github-actions github-actions Bot added the ✨ Introduce new features Introduce new features. label Aug 15, 2026
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>
@nstarman nstarman changed the title ✨ feat(curveframes): reject a two-argument curve at builder construction ✨ feat(curveframes): frames on a curve that changes in time Aug 15, 2026
@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.55%. Comparing base (ddd8c28) to head (5ce5a66).
⚠️ Report is 1 commits behind head on main.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Introduce new features Introduce new features.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants