Summary
min_jerk.py covers one family of reach: a trajectory with a deadline, which
eases in from rest and eases out to rest over a duration T. That is the right
model when the generator is what moves the effector, or when you want a reach
that visibly commits to arriving at a particular time.
It is the wrong model for a whole other family: a servo. Proposing
exponential.py (name open) for the case where the command is simply
"head at the target, at a speed proportional to how far away it is":
v(t) = k · (target − position(t))
No deadline, no duration, no plan. Distance then decays as
d(t) = d₀·exp(−k·t), and the effector is always pointed exactly at the target.
Why it is worth having alongside min-jerk
The two make genuinely different claims and produce visibly different
trajectories, so which one you assume matters:
|
min-jerk |
exponential approach |
| speed profile |
symmetric bell, peaks at τ = 0.5 |
front-loaded, peaks early |
| peak / mean speed |
1.875 |
~1.5 with a modest onset ramp |
| arrival |
eases to rest at the target |
never formally arrives; asymptotic |
| deadline |
T, and the plan is built around it |
none |
| disturbance response |
re-plan; how hard it corrects depends on time remaining |
re-points immediately; correction depends only on distance |
| parameters |
peak_speed, ballistic_duration, min_horizon |
k, plus an onset time constant |
The disturbance row is the interesting one. Min-jerk's re-planning derives its
correction from the time remaining, which is a strong and specific assumption —
push the effector late in a reach and it corrects hard because the deadline is
near. An exponential approach has no deadline to be near, so its correction
depends only on where the effector is. Neither is universally right; having both
lets a caller choose, and lets an analysis compare them.
It is also the natural model for anything servo-like: a cursor assist, a
proportional controller, a smoothing filter pulling toward a setpoint. Those are
common enough that reaching for min-jerk and then explaining why the trajectory
does not ease out is a recurring papercut.
The one wrinkle: pure proportional control starts with a step
v = k·d at t=0 commands full speed immediately, which is not what a real
effector does and produces an infinite-acceleration onset. Two ways out, both
one parameter:
- First-order lag on the command —
v_cmd low-passed with time constant
tau_onset. Cheap, O(1) per sample, matches the EWMA primitive already used
elsewhere in the ezmsg ecosystem.
- Critically-damped second order toward the target, which gives the same
shape from a different derivation and has no separate smoothing stage.
Either yields the characteristic profile: eases in from rest, peaks at roughly a
quarter of the way through the approach, then decays exponentially — still
moving when it crosses a tolerance radius around the target.
API sketch
Mirroring min_jerk.py's shape so the two are drop-in alternatives — stdlib
only, scalar in / scalar out, dataclass-backed state:
@dataclass
class ExponentialReach:
target_x: float = 0.0
target_y: float = 0.0
k: float = 1.0 # 1/s; time constant of the approach is 1/k
tau_onset: float = 0.0 # 0 disables the onset lag (pure proportional)
max_speed: float = 0.0 # 0 disables the clamp
cmd_vx: float = 0.0
cmd_vy: float = 0.0
t_prev: float = 0.0
active: bool = False
def begin_reach(state, t0, target_x, target_y, k, *, tau_onset=0.0, max_speed=0.0) -> None: ...
def step(state, t, pos_x, pos_y, movement_allowed: bool) -> tuple[float, float]: ...
step keeping the same signature as min-jerk's is the point — a caller should be
able to swap the model without touching its loop.
Notes
- Unlike min-jerk there is no
ReplanSeed question. There is no plan to
continue from, so the command depends only on the current position; the
COMMANDED / MEASURED distinction has nothing to attach to.
- A tolerance radius is worth documenting rather than implementing: since the
approach is asymptotic, "arrived" is a caller-side decision and baking a
threshold in would make the model less reusable.
- Happy to send a PR if the shape looks right — checking on the name and on
whether the onset should be option 1 or option 2 before writing it.
Summary
min_jerk.pycovers one family of reach: a trajectory with a deadline, whicheases in from rest and eases out to rest over a duration
T. That is the rightmodel when the generator is what moves the effector, or when you want a reach
that visibly commits to arriving at a particular time.
It is the wrong model for a whole other family: a servo. Proposing
exponential.py(name open) for the case where the command is simply"head at the target, at a speed proportional to how far away it is":
No deadline, no duration, no plan. Distance then decays as
d(t) = d₀·exp(−k·t), and the effector is always pointed exactly at the target.Why it is worth having alongside min-jerk
The two make genuinely different claims and produce visibly different
trajectories, so which one you assume matters:
T, and the plan is built around itpeak_speed,ballistic_duration,min_horizonk, plus an onset time constantThe disturbance row is the interesting one. Min-jerk's re-planning derives its
correction from the time remaining, which is a strong and specific assumption —
push the effector late in a reach and it corrects hard because the deadline is
near. An exponential approach has no deadline to be near, so its correction
depends only on where the effector is. Neither is universally right; having both
lets a caller choose, and lets an analysis compare them.
It is also the natural model for anything servo-like: a cursor assist, a
proportional controller, a smoothing filter pulling toward a setpoint. Those are
common enough that reaching for min-jerk and then explaining why the trajectory
does not ease out is a recurring papercut.
The one wrinkle: pure proportional control starts with a step
v = k·datt=0commands full speed immediately, which is not what a realeffector does and produces an infinite-acceleration onset. Two ways out, both
one parameter:
v_cmdlow-passed with time constanttau_onset. Cheap, O(1) per sample, matches the EWMA primitive already usedelsewhere in the ezmsg ecosystem.
shape from a different derivation and has no separate smoothing stage.
Either yields the characteristic profile: eases in from rest, peaks at roughly a
quarter of the way through the approach, then decays exponentially — still
moving when it crosses a tolerance radius around the target.
API sketch
Mirroring
min_jerk.py's shape so the two are drop-in alternatives — stdlibonly, scalar in / scalar out, dataclass-backed state:
stepkeeping the same signature as min-jerk's is the point — a caller should beable to swap the model without touching its loop.
Notes
ReplanSeedquestion. There is no plan tocontinue from, so the command depends only on the current position; the
COMMANDED/MEASUREDdistinction has nothing to attach to.approach is asymptotic, "arrived" is a caller-side decision and baking a
threshold in would make the model less reusable.
whether the onset should be option 1 or option 2 before writing it.