Generated: 2026-06-23 (EDT) · long-form derivation ·
supersedes the scattered chat summaries. Reads:
validation/analytic_ff_floor_report.py,
validation/nu_e_residual_diagnostic.py,
GNC/breve_controller.py,
GNC/guidance/analytic_feedforward.py,
GNC/equations/current_sota.md §4–5.
If you read one paragraph. We measured an end-effector position error \(p_e \approx 0.107\) m at cruise and asked: is that a tuning failure or an architectural floor? The controller logs a floor of \(0.044\) m, making \(p_e\) look \(2.46\times\) too large. This document shows that the \(0.044\) number and the mysterious “\(54°\)” are the same fact wearing two hats: the floor formula is fed a desired velocity \(\breve v_{des}\) that the end-effector never actually settles to — it is \(54°\) off — so \(0.044\) is the floor of a trajectory we do not fly. Recomputed with the velocity the end-effector does track, the floor is \(0.123\) m and \(p_e \approx 0.107\) sits just under it. We are at the floor.
Two threads got tangled in the chat, so let me separate them cleanly and then tie them back together on purpose.
The punchline of this document is that Thread A and Thread B are the same phenomenon measured two ways. Once you see that, both the \(0.044\) and the \(54°\) stop being mysterious.
The mission is to minimise the end-effector (EE) tracking error \(p_e = \lVert \bm{p}_e - \bm{p}_{ed}\rVert\) — the gap between where the camera is and where the schedule wants it. At cruise we measure \(p_e \approx 0.107\) m and it will not tune away. So we ask the question every controls engineer eventually asks:
Is this \(0.107\) m a tuning failure (a gain is too soft, fix it) or an architectural floor (the dynamics forces a residual that no feedforward can remove)?
A floor is the second thing. In a stiffness-controlled manipulator the steady-state pose error is not zero even with perfect feedforward, because the reduced equations of motion carry residual forcing — Coriolis and center-of-mass (CoM) coupling terms — that the stiffness must physically hold against. The pose error settles to whatever value makes the stiffness force balance that residual. That balance point is the floor. If we are at it, the honest answer is “this is the architectural limit; to go lower you must change the architecture (stiffer EE loop, different impedance), not the tuning.”
So computing the floor honestly is the whole game. Let us derive it.
The controller works in the reduced /
circumcentroidal coordinates of Giordano (2019): a \(9\)-vector velocity \(\breve{\bm v} = [\bm v_c;\ \bm\omega_b;\
\bm\nu_e^\oplus]\) (CoM velocity, base angular rate, reduced EE
twist) and a reduced pose error \(\tilde{\bm
x}\). The working-control law assembles the right-hand side
RHS34b (GNC/breve_controller.py:377-411).
Writing \(\breve{\bm M}\dot{\breve{\bm v}} =
\bm{\mathrm{RHS}}\), the terms are
\[ \breve{\bm M}\dot{\breve{\bm v}} = -\breve{\bm C}\,\breve{\bm v} \;-\; \breve{\bm D}\,(\breve{\bm v}-\breve{\bm v}_{des}) \;-\; \bm J_x^\top \breve{\bm K}\,\tilde{\bm x} \;-\; (\bm C_c + \breve{\bm D}\,\breve{\bm G}_{vc})\,\dot{\tilde{\bm x}}_c \;+\;\cdots \]
with names and provenance:
RHS34b line 400; with
velocity_ff off the term is the
absolute \(-\breve{\bm C}\breve{\bm
v}\), not the error form, line 398);The two key vectors here are \(\breve{\bm v}_{des}\) (the velocity we command) and \(\tilde{\bm x}\) (the pose error we are trying to keep small). Everything below turns on what \(\breve{\bm v}_{des}\) actually is.
Scratch work (working backward). We want the steady-state pose error \(\tilde{\bm x}_{ss}\). “Steady state at cruise” means the reduced acceleration vanishes, \(\dot{\breve{\bm v}}\to\bm 0\), so the right-hand side must sum to zero. If in addition the loop has done its job and the realised velocity has settled onto the commanded one,
\[ \breve{\bm v} \;\longrightarrow\; \breve{\bm v}_{des}, \tag{$\star$ the load-bearing assumption} \]
then two simplifications happen at once: the damping term \(\breve{\bm D}(\breve{\bm v}-\breve{\bm v}_{des})\) vanishes, and the absolute Coriolis term becomes \(-\breve{\bm C}\,\breve{\bm v}_{des}\). What remains is a balance between the stiffness and the residual forcing.
The derivation. Setting the right-hand side to zero under \((\star)\),
\[ \bm 0 = -\breve{\bm C}\,\breve{\bm v}_{des} \;-\; \bm J_x^\top \breve{\bm K}\,\tilde{\bm x}_{ss} \;-\; (\bm C_c + \breve{\bm D}\,\breve{\bm G}_{vc})\,\dot{\tilde{\bm x}}_c. \]
Moving the stiffness term to one side and inverting \(\bm J_x^\top \breve{\bm K}\) (a least-squares solve in code) gives the cruise-lag floor:
\[ \boxed{\; \tilde{\bm x}_{ss} = -\big(\bm J_x^\top \breve{\bm K}\big)^{-1} \Big[\;\underbrace{\breve{\bm C}\,\breve{\bm v}_{des}}_{\text{Coriolis forcing}} \;+\; \underbrace{(\bm C_c + \breve{\bm D}\,\breve{\bm G}_{vc})\,\dot{\tilde{\bm x}}_c}_{\text{CoM-coupling forcing}}\;\Big] \;} \tag{4.14} \]
This is exactly cruise_lag_floor
(GNC/guidance/analytic_feedforward.py:144-158) and
x_ss_floor (GNC/breve_controller.py:352-375);
it mirrors current_sota.md §4.5. The reported floor is the
EE-position block of this reduced error,
\[ \text{floor\_pe} = \big\lVert \tilde{\bm x}_{ss}[\,3{:}6\,] \big\rVert, \]
(breve_controller.py:373). The moral of the derivation:
the floor is the position error the stiffness must hold to balance a
forcing it cannot remove — and the Coriolis half of that forcing is
\(\breve{\bm C}\,\breve{\bm v}_{des}\),
which depends entirely on the commanded velocity \(\breve{\bm v}_{des}\).
Read the code that builds it
(breve_controller.py:358-360):
\[ \breve{\bm v}_{des} = \big[\, \bm\omega_{b,des}\, ;\ \bm\nu_e^{\oplus},_{des}\, \big]. \]
The EE half, \(\bm\nu_e^{\oplus},_{des}\), is the desired reduced EE twist — the feedforward. And here is the fork that this whole investigation lives on: there are two different feedforwards in this codebase, and they put different vectors into \(\breve{\bm v}_{des}\):
| run | how \(\bm\nu_{e,des}\) is built | one-line meaning |
|---|---|---|
FD
(desired_nu_e_feedforward_consistent,
ee_guidance.py:225-261) |
\(\bm\nu_{e,des} = \bm R_{te}^\top\,(\bm p_{e,des}^{(k)}-\bm p_{e,des}^{(k-1)})/\Delta t\) | literally \(\mathrm{d}/\mathrm{d}t\) of the committed pose |
analytic
(analytic_desired_nu_e_feedforward,
ee_guidance.py:267-306) |
\(\bm\nu_{e,des} = \bm R_{ed}^\top\,\dot{\bm p}_{ed}\), closed form (eqs 5.7–5.14) | velocity of the ideal standoff trajectory |
So the floor (4.14) is not one number — it is a function of which feedforward you fed it. Hold that thought.
Run validation/analytic_ff_floor_report.py on the two
matched runs (run_PM0523, operational, non-derated window).
The literal output:
| metric | FD (off) | analytic (on) |
|---|---|---|
| \(p_e\) median (m) | 0.1059 | 0.1074 |
| floor (m) | 0.1229 | 0.0437 |
| ratio \(p_e/\text{floor}\) | 0.86 | 2.46 |
So the original floor number was \(0.044\) m — the floor logged in the analytic run. Against \(p_e = 0.107\) m it gives the headline \(0.107/0.044 = 2.46\times\) “above the floor,” which read as a \(0.063\) m gap to close.
Now stare at the two columns. The actual \(p_e\) is the same in both runs (\(0.106\) vs \(0.107\) — the feedforward choice does not move where the EE sits). The only thing that changed is the floor prediction: \(0.123\) in FD, \(0.044\) in analytic. By §4 that can only be because \(\breve{\bm v}_{des}\) — the Coriolis forcing \(\breve{\bm C}\breve{\bm v}_{des}\) — is different between the two. We are about to see why it is different, and that “why” is the \(54°\).
The floor (4.14) is only trustworthy if its assumption \((\star)\), \(\breve{\bm v}\to\breve{\bm v}_{des}\),
actually holds. So we tested it the obvious way: compare the
realised EE twist \(\bm\nu_e\) against the
desired EE twist \(\bm\nu_{e,des}\) over the cruise window
(validation/nu_e_orthogonality.py). The result, in the
EE-local frame:
\[ \lVert\bm\nu_e\rVert \approx 0.847, \qquad \lVert\bm\nu_{e,des}\rVert \approx 0.863, \qquad \angle(\bm\nu_e,\bm\nu_{e,des}) \approx 54°. \]
(An earlier note called this “\(\sim\)orthogonal / \(90°\)”; that was an over-reading of “\(\lVert\text{error}\rVert\approx\lVert\text{desired}\rVert\).” The honest per-step cosine is \(54°\), confirmed by the law of cosines on the norms.) This is the \(54°\). It says: the realised EE velocity has the right speed but points \(54°\) off the feedforward direction. Matched magnitude, wrong heading.
Taken at face value, \((\star)\) looks broken: \(\breve{\bm v}\neq\breve{\bm v}_{des}\) by \(54°\), so the floor’s premise fails and that “must” be why \(p_e\) exceeds the floor. This face-value reading is the trap. It conflates two different “desired velocities,” and untangling them is TASK A.
Here is the subtlety. The vector \(\bm\nu_{e,des}\) we compared against is the feedforward twist. It is not, in general, the time-derivative of the desired pose \(\dot{\bm p}_{ed}\) that the EE is actually asked to follow. There are two distinct “desired EE velocities” in this system, and we must not confuse them:
To find out which one the EE actually tracks, we sidestepped the
feedforward entirely and differenced the logged
positions
(validation/nu_e_residual_diagnostic.py). Because the full
EE rotation \(\bm R_{te}\) is not
logged, we work in the world frame, where the angle between two vectors
is frame-invariant. Define the realised velocity, the desired-pose
velocity, and their difference:
\[ \bm v_{act} = \tfrac{\mathrm d}{\mathrm dt}\bm p_e,\qquad \bm v_{des} = \tfrac{\mathrm d}{\mathrm dt}\bm p_{ed},\qquad \bm\Delta = \bm v_{act}-\bm v_{des}. \]
The measurement:
\[ \lVert\bm\Delta\rVert \approx 0.0135\ \text{m/s} \;=\; 1.6\%\ \text{of the } 0.85\ \text{m/s cruise},\qquad \angle(\bm\Delta,\;\bm p_e\text{-error}) \approx 90°. \]
A frame-invariant cross-check seals it: the world-frame speed \(\lVert\bm v_{act}\rVert = 0.8475\) equals the logged EE-local \(\lVert\bm\nu_e\rVert = 0.8475\) to the digit. So the EE tracks the desired-pose velocity \(\dot{\bm p}_{ed}\) almost perfectly (to \(1.6\%\)). The \(54°\) is therefore not between the EE and the velocity it needs to follow the commanded pose — it is between the EE and the feedforward signal, and
\[ \bm\nu_{e,des}^{\text{(analytic)}} \;\neq\; \tfrac{\mathrm d}{\mathrm dt}\bm p_{ed}. \]
The clinching A/B. In the FD run, \(\bm\nu_{e,des}\) is defined as \(\mathrm d/\mathrm dt\,\bm p_{ed}\) (table, §4). There the same \(54°\) collapses to \(0.91°\). Only the analytic feedforward produces the \(54°\). So the \(54°\) is a property of the analytic feedforward construction, full stop.
A \(5\)-agent investigation
(parallel code-readers \(\to\)
synthesis \(\to\) adversarial
refutation) classified the \(54°\) as
an intentional different quantity, not a bug. The analytic
feedforward (analytic_feedforward.py,
current_sota.md §5) is the closed-form velocity of the
ideal standoff trajectory: the camera held at radius
\(r_{cam}\) from the on-orbit-projected
desired CoM \(\bm p_{cd}\), aimed at
the moving surface point \(\bm
x_{surf}\). With \(\bm r = \bm
x_{surf}-\bm p_{cd}\), \(\rho=\lVert\bm
r\rVert\), \(\bm u=\bm
r/\rho\):
\[ \bm p_{ed} = \bm p_{cd} + r_{cam}\,\bm u,\qquad \dot{\bm u} = \tfrac1\rho\big(\bm I - \bm u\bm u^\top\big)\dot{\bm r},\qquad \dot{\bm p}_{ed} = \bm v_{cd} + r_{cam}\,\dot{\bm u}. \tag{5.7,\,5.8,\,5.10} \]
This is a clean, idealised velocity. The
committed pose \(\bm
p_{ed}\) that the EE actually tracks is built differently
(ee_guidance.py:687-692, 792-892): it is anchored on the
measured CoM (not the desired \(\bm p_{cd}\)) and then pushed through a
first-order low-pass, a per-step rate clamp, and a reach clamp — none of
which the analytic feedforward sees. Three gaps stack (CoM anchor,
smoothing, and the rotation frame \(\bm
R_{ed}\) vs \(\bm R_{te}\)), and
together they tilt the analytic twist \(54°\) in azimuth (\(+y\) over \(+x\)) from \(\mathrm d/\mathrm dt\,\bm p_{ed}\) while
keeping the same magnitude (both are dominated by the common cruise term
\(\bm v_{cd}\)).
One honest caveat (the adversarial check earned its keep): the per-term split of the \(54°\) — how much is the reference difference versus the \(\bm R_{ed}\)-vs-\(\bm R_{te}\) frame — is not resolvable from the current logs, because boresight roll is unconstrained and the diagnostic compares logged-desired against logged-actual. The classification is solid; the attribution needs a re-run with \(\bm R_{ed}\) logged. It does not change anything below.
Now we collapse the two threads. Recall from §4 that the floor’s Coriolis forcing is \(\breve{\bm C}\,\breve{\bm v}_{des}\), and \(\breve{\bm v}_{des}\) carries the feedforward twist \(\bm\nu_{e,des}\). Recall the load-bearing assumption \((\star)\): \(\breve{\bm v}\to\breve{\bm v}_{des}\). From §7 we now know the EE actually settles to
\[ \breve{\bm v} \;\longrightarrow\; \tfrac{\mathrm d}{\mathrm dt}\bm p_{ed} \qquad(\text{the committed-pose velocity, tracked to }1.6\%). \]
In the analytic run the commanded \(\breve{\bm v}_{des}\) is the ideal-standoff twist, which is \(54°\) away from \(\mathrm d/\mathrm dt\,\bm p_{ed}\). Therefore
\[ \breve{\bm v}\;\to\;\tfrac{\mathrm d}{\mathrm dt}\bm p_{ed}\;\neq\;\breve{\bm v}_{des}^{\text{(analytic)}} \qquad\Longrightarrow\qquad (\star)\ \textbf{is violated by exactly the } 54°. \]
So when the floor formula (4.14) evaluates \(\breve{\bm C}\,\breve{\bm v}_{des}\) with the analytic twist, it computes the steady-state error of a counterfactual — a world in which the EE flies the ideal-standoff velocity. It never does. That counterfactual floor is \(0.044\) m. The system instead flies \(\mathrm d/\mathrm dt\,\bm p_{ed}\), whose floor is the FD number, \(0.123\) m — and the realised \(p_e = 0.107\) m sits right under it (ratio \(0.86\)).
\[ \boxed{\;0.044\ \text{m (analytic floor)} \;=\; \text{the floor of a velocity the EE is } 54° \text{ from flying.}\;} \]
The \(0.044\) and the \(54°\) are the same fact. The \(54°\) is the angle by which \(\breve{\bm v}_{des}^{\text{(analytic)}}\) misses the velocity the EE settles to; the \(0.044\) is what the floor formula returns when you feed it that \(54°\)-wrong \(\breve{\bm v}_{des}\). Feed the floor the velocity the EE does settle to (the FD twist) and you get \(0.123\), against which \(p_e=0.107\) is at the floor.
flowchart TD
subgraph SCHED["Committed pose schedule (what the EE tracks)"]
P["desired pose p_ed\n(measured-CoM anchored,\nlow-pass + rate + reach clamp)"]
P -->|"d/dt"| VPOSE["v_des = d/dt(p_ed)\n||.|| approx 0.85 m/s"]
end
subgraph FF["Two feedforwards feed v_breve_des"]
AN["analytic FF twist\n(ideal-standoff velocity,\neq 5.7-5.14)"]
FDp["FD FF twist\n:= d/dt(p_ed) by construction"]
end
ACT["realised EE twist nu_e\n(EE tracks v_des to 1.6%)"]
AN -. "54 deg off" .-> VPOSE
FDp -- "0.91 deg (same vector)" --> VPOSE
ACT -- "tracks" --> VPOSE
FLOOR["FLOOR eq 4.14:\nx_ss = -(Jx^T Kbreve)^-1 [ Cbreve v_breve_des + CoM-coupling ]\nfloor_pe = ||x_ss[3:6]||"]
AN -->|"v_breve_des = analytic"| FLOOR
FDp -->|"v_breve_des = FD"| FLOOR
FLOOR -->|"analytic v_des (54 deg off what EE flies)"| F044["floor = 0.044 m\nphantom (counterfactual)"]
FLOOR -->|"FD v_des (what EE flies)"| F123["floor = 0.123 m\nhonest"]
F044 --> RATIO["p_e 0.107 / 0.044 = 2.46x\n(the ghost gap)"]
F123 --> ATFLOOR["p_e 0.107 / 0.123 = 0.86\nAT the floor"]
The moral. A “floor” is only as honest as the desired velocity you feed it. The cruise-lag floor (4.14) assumes the robot settles onto \(\breve{\bm v}_{des}\). If \(\breve{\bm v}_{des}\) is a feedforward for a different trajectory than the one the loop tracks, the floor formula faithfully computes the steady-state error of a journey nobody takes. Our analytic feedforward is exactly such a different trajectory (the ideal standoff), \(54°\) off the committed-pose velocity the EE actually flies — so the floor it produces (\(0.044\) m) is a phantom. Read with the velocity the EE truly settles to, the floor is \(0.123\) m, and \(p_e\approx0.107\) m is sitting on it. We are at the architectural cruise-lag floor; the \(2.46\times\) gap was a measurement artifact, not a tuning target.
What teaches more than the clean result is how it fooled us: matched magnitudes (\(\lVert\bm\nu_e\rVert\approx\lVert\bm\nu_{e,des}\rVert\)) made the feedforward look like the right reference, and a feedforward that is wrong only in direction leaves \(p_e\) untouched (the stiffness still holds the same balance) while quietly corrupting the floor prediction. The error hides in the prediction, not the plant.
current_sota.md eq 5.7 writes \(\bm p_{ed}=\bm p_c - r_{cam}\hat{\bm r}\)
(minus), but its own derivative eq 5.10 uses \(+\,r_{cam}\dot{\hat{\bm r}}\) and the code
uses \(\bm p_{ed}=\bm p_{cd}+r_{cam}\bm
u\) (analytic_feedforward.py:131, plus). The \(+\) is correct; eq 5.7’s \(-\) is the typo. Left for you to fix in the
canonical sheet.ee_guidance.py:283. Scientific completeness only; it does
not change the verdict.frame_angle_diag run, Jun 23) — and an
honest correctionThe §12 open question is now measured, and it sharpens §7–9 while tempering §13.
The \(54°\) is a frame roll,
not a heading error. A diagnostic run
(frame_angle_diag, run_PM1013,
validation/frame_angle_readout.py) logged the geodesic
angle between the desired EE frame \(\bm
R_{ed}=\bm R_e^{des}\) and the realised \(\bm R_{te}\). Over the operational window
the medians are
\[ \theta_{frame}=\big\lVert\mathrm{so3\_log}(\bm R_{ed}^\top\bm R_{te})\big\rVert \approx 53.8°, \qquad \text{boresight pointing}\approx 2.2°, \qquad \theta_{frame}/\text{pointing}\approx 24.6. \]
So the desired and realised frames aim at nearly the same
point (pointing \(2.2°\)) but
are rolled \(\sim\!52°\) about
the boresight. The “\(54°\)
heading offset” in \(\bm\nu_e\) is that
roll re-expressing the same world velocity in a \(54°\)-rotated \(x\)–\(y\)
basis — a frame/metric artifact, not a physical tracking
error. Its root is structural: axis_only_attitude
zeros \(w_z\), so the controller never
commands EE roll (the camera is axisymmetric — roll does not change what
it sees), and \(\bm R_{ed}\) (whatever
roll the analytic standoff geometry picks) and \(\bm R_{te}\) (whatever roll the arm settles
into) drift freely apart. The \(\bm\nu_e\) tracking metric is therefore
ill-posed (it compares across rolled frames); judge EE tracking
by the world-frame \(p_e\) and
pointing. (Practical upshot: the smoothing-reduction idea is not the
lever for this \(54°\).)
Honest correction to §5/§9/§13. The “we are
at the floor (\(0.123\) m)”
line compared the FD run’s floor against the
analytic run’s \(p_e\)
— a cross-run comparison. The within-run honest floor, logged here as
floor_pe_realized (eq 4.14 at the realised reduced
velocity, in this run’s matrices), is \(\approx 0.055\) m, and \(p_e\approx0.107\) m sits \(\mathbf{1.96\times}\) above it (vs \(2.46\times\) above the analytic floor \(0.044\)). So within the analytic
run, \(p_e\) is \(\sim\!2\times\) above even the honest
floor — the realised-velocity floor is a better
predictor than the analytic one but does not say “at
the floor.” The clean apples-to-apples number is the within-run
consistent floor (eq 4.14 at \(\mathrm
d/\mathrm dt\,\bm p_{ed}\), which floor_pe_realized
only approximates via the realised velocity); computing it is the
remaining follow-up. The §9 mechanism (the analytic floor is fed an
un-tracked \(\breve{\bm v}_{des}\))
stands; the quantitative “sits on the floor” claim is softened
to “\(\sim\!2\times\) above the honest
floor, pending the consistent-floor number.”
The §14 follow-up is now measured. A new opt-in
diagnostic floor_pe_consistent
(breve_controller.py, under the existing
log_cruise_lag_floor guard) evaluates eq 4.14 fed the
velocity the EE actually tracks — the committed-pose velocity
\(\mathrm d/\mathrm dt\,\bm p_{ed}\) in
the realized frame \(\bm R_{te}\)
(side-effect-free; never writes des.nu_e). A fresh 2×2
(floor_consistent_{an,fd}, full helix, operational window)
settles it:
| run | \(p_e\) | analytic floor | realized floor | consistent floor | \(p_e/\)consistent | \(\theta_{frame}\) |
|---|---|---|---|---|---|---|
| analytic (adopted) | 0.1074 | 0.0437 (2.46×) | 0.0548 (1.96×) | 0.1022 | 1.05 | 53.8° |
| FD | 0.1059 | 0.1229 (0.86×) | 0.0682 (1.55×) | 0.1229 | 0.86 | 40.7° |
Verdict: \(p_e\) is AT the architectural cruise-lag floor — \(1.05\times\) the consistent floor in the analytic (adopted) run, \(0.86\times\) in the FD run. Both the \(2.46\times\) (analytic floor) and the \(1.96\times\) (realized floor) were artifacts of feeding eq 4.14 a velocity the EE never settles to:
Internal cross-check (passes): in the FD run the
controller’s \(\bm\nu_{e,des}\)
is \(\mathrm d/\mathrm dt\,\bm
p_{ed}\) by construction, so floor_pe and
floor_pe_consistent must coincide — they do, to the digit
(\(0.1229\)). That validates the new
diagnostic against the existing FD-mode floor.
The §14 softening is now removed: the realized-floor
“\(\sim\!2\times\) above” was itself a
frame artifact (the rolled \(\bm
R_{te}\) in nu_e_local_to_oplus), not a real gap.
The honest, frame-consistent, single-run number is the consistent floor,
and \(p_e\approx0.107\) m is at it.
No feedforward lever lowers \(p_e\) (the FF only changes the
prediction, not the plant — fd and an reach the same \(p_e\)); the only lever is the EE position
stiffness \(\breve{\bm K}\), a separate
axis with its own actuator-effort and singularity-margin trade.
Otherwise: document the limit and ship the nominal.
Backing: validation/frame_angle_readout.py (now
logs the consistent floor) on
logs/logs_Jun23_26/floor_consistent_{an,fd}/.../run_PM1136/;
the boresight-roll cause is figured in
validation/nu_e_roll_figures.py (Fig 1 the \(\|\bm\nu_e-\bm\nu_{e,des}\|\) magnitude;
Fig 2 the roll proof — \(\theta_{frame}\,53.8°\) vs pointing \(2.2°\), and the \(59\times\) world-frame collapse).
Diagnostic commit e2d4379.