Skip to content

Expose the planned reach alongside the commanded one - #1

Merged
cboulay merged 1 commit into
devfrom
nominal-trajectory
Aug 24, 2026
Merged

Expose the planned reach alongside the commanded one#1
cboulay merged 1 commit into
devfrom
nominal-trajectory

Conversation

@cboulay

@cboulay cboulay commented Aug 24, 2026

Copy link
Copy Markdown
Member

Why

Once step() re-plans against the measured position, its output stops being a description of the reach and becomes a description of the reach plus whatever the effector did. That is the right thing to send to the effector and the wrong thing to record as ground truth, and there was no way to get the latter back out.

Integrating the commanded velocity does not recover it. Each step's velocity is anchored to the effector rather than to the integral, so integrating a command issued to a lagging effector runs past the target and keeps going — the command is correct in the effector's frame and meaningless outside it.

What

Two new functions, plan_position(state, t) and plan_velocity(state, t), giving the reach as planned at the cue, on the original start→target axis, unconditioned on what happened.

  • They evaluate the same polynomial step() already uses for its ballistic phase, over the whole reach rather than only over ballistic_duration. Through that phase the command and the plan are the same function to machine precision.
  • Afterwards they separate by exactly the correction re-planning applied, so their difference is the effector's tracking error — the useful part, and not recoverable from either signal alone.
  • plan_position returns None for an inactive reach rather than the origin, because (0, 0) is a real coordinate and cannot double as "no plan". step() can return (0, 0) for "no command" since rest is the unambiguous zero of a velocity; position has no such zero. A caller wanting an unbroken signal substitutes the effector's own position — with no reach, where it is is where it should be. plan_velocity does return (0, 0), matching step().
  • Time outside the reach clamps rather than extrapolating: before the cue gives the start point, past the nominal duration gives the target.

Purely additive — no change to step(), begin_reach(), or reset().

Tests

New TestNominalTrajectory class covering the endpoints, the canonical min-jerk curve, clamping, the None-vs-origin distinction, independence from the effector's actual path, plan_velocity as the numerical derivative of plan_position, exact agreement with the ballistic command, command and plan staying together for an undisturbed reach, and separating for a disturbed one.

50 tests pass.

🤖 Generated with Claude Code

Once step() re-plans against the measured position, its output stops being a
description of the reach and becomes a description of the reach plus whatever
the effector did. That is the right thing to send to the effector and the
wrong thing to record as ground truth, and there was no way to get the latter
back out.

Integrating the commanded velocity does not recover it. Each step's velocity
is anchored to the effector rather than to the integral, so integrating a
command issued to a lagging effector runs past the target and keeps going --
the command is correct for the effector's frame and meaningless outside it.

plan_position and plan_velocity evaluate the reach as planned at the cue, on
the original start-to-target axis, unconditioned on what happened. They are
the same polynomial step() already uses for its ballistic phase, evaluated
over the whole reach rather than only over ballistic_duration, so through that
phase the command and the plan are the same function to machine precision.
Afterwards they separate by exactly the correction re-planning applied, which
makes their difference the effector's tracking error -- the useful part, and
not recoverable from either signal alone.

plan_position returns None for an inactive reach rather than the origin,
because 0,0 is a real coordinate and cannot double as "no plan". step() can
return 0,0 for no command since rest is the unambiguous zero of a velocity;
position has no such zero. A caller wanting an unbroken signal substitutes the
effector's own position -- with no reach, where it is is where it should be.
plan_velocity does return 0,0, matching step().

Time outside the reach clamps rather than extrapolating, so a query before the
cue gives the start point and one past the nominal duration gives the target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cboulay
cboulay merged commit ca7c837 into dev Aug 24, 2026
17 checks passed
@cboulay
cboulay deleted the nominal-trajectory branch August 24, 2026 23:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant