Date: Jun 14, 2026. Status of the
refactor: desired_nu_e_feedforward_consistent
moved from the controller into the guidance layer
(EEGuidance), byte-identical — all 5
baseline validators report max_abs_diff = 0.000e+00 (base /
EE-INITIAL / EE-TARGETING / EE-COVERAGE / EE-TRACKING). The computation
is still finite-difference (noisy) for now; relocation only. This doc
maps the feedforward path and sets up the v2 derivation (analytic ν̇ from
the trajectory).
ν_des and ν̇_des are trajectory quantities. With the function in guidance, the trajectory generator can later supply them analytically instead of the controller finite-differencing its own position reference twice (FD-of-FD = the noisy accel-FF term). The move is the prerequisite; the analytic feedforward is L1 v2 proper.
| piece | location | role |
|---|---|---|
| ν_des (velocity FF reference) | ee_guidance.py:217
desired_nu_e_feedforward_consistent |
FD of des.p_e (+ des.R_e) → desired EE
local twist. Now in guidance. Uses the shared
ee_cache.R_te. |
| call site | breve_controller.py:123 | self.nu_e.local.des = self.traj.desired_nu_e_feedforward_consistent(...) |
| ν_des → reduced frame | breve_controller.py:568
nu_e_local_to_oplus |
local twist → coordinated (⊕) frame |
| ν̇_des (accel FF, FD-of-FD) | breve_controller.py:588
update_desired_nu_e_oplus_feedforward |
oplus.dot = (oplus.des − oplus.prev)/dt — the
noise-dither source |
| control law | breve_controller.py:396
RHS34b |
assembles K, D, C, COM-coupling, posture, + accel FF |
| damping refs ν_des | breve_controller.py:190
v_breve_damping_error |
ν − ν_des (velocity FF already partly here) |
| accel FF term | breve_controller.py:390
rhs_feedforward |
M_breve @ ν̇_des |
| gains (derate-scaled) | breve_controller.py:385
arm_breve_gains |
scale·D, scale·K; scale = near-singularity
derate (×0.25) |
if self.arm_cfg.desired_twist.velocity_ff: # L1 v1 (default OFF)
v_breve_des = sanitize_column(np.vstack([as_col3(des.omega_b), self.nu_e.oplus.des]))
rhs["C_breve"] = -(C_breve @ (v_breve - v_breve_des)) # Coriolis referenced to v_des
else:
rhs["C_breve"] = -(C_breve @ v_breve) # absolute Coriolis (baseline)
rhs["D_breve"] = -(D_breve @ v_breve_err) # v_breve_err = v - v_des (damping FF)
rhs["K_breve"] = -(J_x.T @ K_breve @ x_tilde) # stiffness restoring
rhs["COM_coupling"] = -((C_c + D_breve @ G_vc_breve) @ x_c_dot_err)
# + posture, then:
return rhs_total + self.rhs_feedforward() # + M_breve @ v_dot_des (accel FF)
# TODO (user): make this clearer — put rhs_feedforward in the rhs dict as its own entry.
We want the standing EE pose error during steady cruise, and which control term carries it.
Cruise does not mean the CoM stops — the system is
moving (the CoM cruises at the commanded desired_speed).
The symbol \(\boldsymbol{v}_c \equiv
\dot{\tilde{\boldsymbol{x}}}_c\) is the CoM
tracking-error rate (eq. 34b’s definition),
not the absolute CoM velocity (which is the code’s
motion.v_c). The CoM loop is decoupled and stable — eq.
34a, \(m\ddot{\tilde{\boldsymbol{x}}}_c +
\boldsymbol{D}_c\dot{\tilde{\boldsymbol{x}}}_c +
\boldsymbol{K}_c\tilde{\boldsymbol{x}}_c = \boldsymbol{0}\), a
damped second-order error system — so the CoM error and its
rate decay to zero, \(\tilde{\boldsymbol{x}}_c
= \boldsymbol{v}_c = \boldsymbol{0}\). That (not any stopping)
removes the CoM-coupling forcing \(-(\boldsymbol{C}_c +
\breve{\boldsymbol{D}}\breve{\boldsymbol{G}}_{v_c})\dot{\tilde{\boldsymbol{x}}}_c\).
With the project’s tracking feedforward added to the canonical regulator
(eq. 34b), RHS34b then realizes
\[ \breve{\boldsymbol{M}}\, \dot{\breve{\boldsymbol{v}}} = -\, \breve{\boldsymbol{C}}\, \breve{\boldsymbol{v}} \;-\; \breve{\boldsymbol{D}}\, (\breve{\boldsymbol{v}} - \breve{\boldsymbol{v}}_{des}) \;-\; \boldsymbol{J}_{\tilde{x}}^{\top}\, \breve{\boldsymbol{K}}\, \tilde{\boldsymbol{x}} \;+\; \breve{\boldsymbol{M}}\, \dot{\breve{\boldsymbol{v}}}_{des}, \]
paired with the tracking error kinematics (the
moving-reference correction — the reason
desired_nu_e_feedforward_consistent exists at all):
\[ \dot{\tilde{\boldsymbol{x}}} = \boldsymbol{J}_{\tilde{x}}\, (\breve{\boldsymbol{v}} - \breve{\boldsymbol{v}}_{des}). \]
Symbols: \(\breve{\boldsymbol{v}} = [\boldsymbol{\omega}_b;\, \boldsymbol{\nu}_e^{\oplus}]\) is the reduced velocity; \(\tilde{\boldsymbol{x}} = [\tilde{\boldsymbol{x}}_b;\, \tilde{\boldsymbol{x}}_e]\) the stacked attitude+EE pose error; \(\breve{\boldsymbol{M}}, \breve{\boldsymbol{C}}, \breve{\boldsymbol{K}}, \breve{\boldsymbol{D}} \in \mathbb{R}^{9 \times 9}\) are the coupled-subsystem inertia, Coriolis, stiffness, and damping; \(\boldsymbol{J}_{\tilde{x}} \in \mathbb{R}^{9 \times 9}\) the error-rate Jacobian.
We want the standing pose error \(\tilde{\boldsymbol{x}}_{ss}\) during steady cruise. “Steady” means the error has stopped moving: \(\dot{\tilde{\boldsymbol{x}}} = \boldsymbol{0}\). Reading that through the error kinematics, and using that \(\boldsymbol{J}_{\tilde{x}}\) is full rank,
\[ \dot{\tilde{\boldsymbol{x}}} = \boldsymbol{0} \;\;\Longrightarrow\;\; \boldsymbol{J}_{\tilde{x}}\, (\breve{\boldsymbol{v}} - \breve{\boldsymbol{v}}_{des}) = \boldsymbol{0} \;\;\Longrightarrow\;\; \breve{\boldsymbol{v}} = \breve{\boldsymbol{v}}_{des}. \]
So at steady cruise the velocity error vanishes — the damping has done its job — and since the demand is (idealized) constant-velocity, \(\dot{\breve{\boldsymbol{v}}} = \dot{\breve{\boldsymbol{v}}}_{des}\).
Substitute \(\breve{\boldsymbol{v}} = \breve{\boldsymbol{v}}_{des}\) and \(\dot{\breve{\boldsymbol{v}}} = \dot{\breve{\boldsymbol{v}}}_{des}\) into the closed loop. The damping term drops (its argument is zero), and the acceleration feedforward \(\breve{\boldsymbol{M}}\dot{\breve{\boldsymbol{v}}}_{des}\) appears on both sides:
\[ \breve{\boldsymbol{M}}\, \dot{\breve{\boldsymbol{v}}}_{des} = -\, \breve{\boldsymbol{C}}\, \breve{\boldsymbol{v}}_{des} \;-\; \boldsymbol{J}_{\tilde{x}}^{\top}\, \breve{\boldsymbol{K}}\, \tilde{\boldsymbol{x}}_{ss} \;+\; \breve{\boldsymbol{M}}\, \dot{\breve{\boldsymbol{v}}}_{des}. \]
The two \(\breve{\boldsymbol{M}}\dot{\breve{\boldsymbol{v}}}_{des}\) terms cancel, leaving a pure force balance: the stiffness term must hold against the Coriolis force of the cruising arm. Solving for the standing error,
\[ \boldsymbol{J}_{\tilde{x}}^{\top}\, \breve{\boldsymbol{K}}\, \tilde{\boldsymbol{x}}_{ss} = -\, \breve{\boldsymbol{C}}\, \breve{\boldsymbol{v}}_{des} \qquad\Longrightarrow\qquad \boxed{\; \tilde{\boldsymbol{x}}_{ss} = -\, \big( \boldsymbol{J}_{\tilde{x}}^{\top} \breve{\boldsymbol{K}} \big)^{-1}\, \breve{\boldsymbol{C}}\, \breve{\boldsymbol{v}}_{des} \;} \]
With perfect velocity and acceleration feedforward, the only standing lag that survives is the one needed to generate the Coriolis force: the stiffness leans into a fixed pose error \(\tilde{\boldsymbol{x}}_{ss}\) to supply \(\breve{\boldsymbol{C}}\breve{\boldsymbol{v}}_{des}\). So the lag is proportional to (Coriolis)/(stiffness) — exactly the code comment’s “\(pe = v\, \tau\)”, now with \(\tau = (\boldsymbol{J}_{\tilde{x}}^{\top} \breve{\boldsymbol{K}})^{-1} \breve{\boldsymbol{C}}\) named explicitly.
This also explains why v1 was inert: referencing the Coriolis to the demand, \(-\breve{\boldsymbol{C}}(\breve{\boldsymbol{v}} - \breve{\boldsymbol{v}}_{des})\), makes the whole RHS vanish at \((\breve{\boldsymbol{v}} = \breve{\boldsymbol{v}}_{des},\ \tilde{\boldsymbol{x}} = \boldsymbol{0})\) — a zero-lag equilibrium. But the term probe measured \(\breve{\boldsymbol{C}} \approx 0.073 \ll \breve{\boldsymbol{K}} \approx 0.299\), so \(\tilde{\boldsymbol{x}}_{ss}\) is tiny at rs 0.40. v1 removes a lag that is already nearly zero.
The box predicts a tiny standing lag (just the small
Coriolis term), yet we measure
pe_med = 0.1475 m. Something the algebra says should
cancel, does not. The suspect is the one idealization: I assumed \(\dot{\breve{\boldsymbol{v}}}_{des}\) is
exact, so \(\breve{\boldsymbol{M}}\dot{\breve{\boldsymbol{v}}}_{des}\)
cancels cleanly. Ours is not exact — it is oplus.dot, a
finite difference of a finite difference of the filtered pose
reference. If \(\dot{\breve{\boldsymbol{v}}}_{des}\) is
biased/noisy, the cancellation is only partial, and the leftover
\[ \breve{\boldsymbol{M}}\, \big( \dot{\breve{\boldsymbol{v}}}_{des}^{\text{true}} - \dot{\breve{\boldsymbol{v}}}_{des}^{\text{FD}} \big) \]
becomes a forcing the stiffness must absorb as extra standing error. That gap — not the Coriolis term — is the likely home of the 0.1475 m, and it is exactly what an analytic \(\dot{\breve{\boldsymbol{v}}}_{des}\) (v2) removes.
Falsifiable prediction: make the feedforward exact
and pe should collapse toward the Coriolis floor \(-(\boldsymbol{J}_{\tilde{x}}^{\top}
\breve{\boldsymbol{K}})^{-1} \breve{\boldsymbol{C}}\,
\breve{\boldsymbol{v}}_{des}\).
OFF byte-identity holds (5 validators = 0). Any v2 change is gated default-OFF, byte-identical when OFF, and validated full-helix (not 200 s — the L3 lesson). The analytic ν̇ will not be byte-identical to FD (that’s the point); it earns adoption only on a full-helix A/B vs the FD baseline with no gate regression.