Doctoral Research · Space Robotics Inspection with a Free-Flying Space Manipulator
A Doctoral Research Journal Aerospace Engineering

The nu_e Situation — what the EE-twist variables are, how they flow, and what is actually dead

Generated: 2026-06-23 (EDT) · structural trace via ast-grep (not text grep) · branch speed-derate. Reads: GNC/breve_controller.py, GNC/breve_core_controller.py, GNC/com_controller.py, GNC/guidance/ee_guidance.py, GNC/guidance/analytic_feedforward.py, GNC/guidance/guidance_rollout.py, utils/robot.py, utils/data_classes.py, YAMLs_by_domain/metrics.yaml.

If you read one paragraph. nu_e is the end-effector reduced twist — a load-bearing physical signal that drives the EE wrench’s damping term and therefore the joint torques. It is not dead code. The only genuinely dead nu_e-adjacent items are tiny: one vestigial state field (nu_e.local.prev, written twice and read nowhere) and one diagnostic log line that is excluded from serialization (nu_e_oplus_des, include:false). A third thing — the logged nu_e tracking metric — is not dead but ill-posed: it compares the realized and desired EE twists across boresight-rolled frames (R_te vs R_ed, ~52° apart), so its famous “54°” is a frame artifact. The earlier “dead-code cleanup” note bundled these and got compressed into the false claim “nu_e is dead.” It is not.


0. Why this report exists

A handoff said the 54° nu_e error was “an irrelevant red herring” and that “nu_e was dead code.” The second half is wrong and worth correcting precisely, because nu_e is the camera’s end-effector twist — remove it and EE damping dies. This document traces the actual flow of the EE-twist variables through the Breve guidance→control system and classifies each as load-bearing, conditionally live, or dead, with ast-grep evidence (a write-vs-read question is structural, so the AST answers it cleanly).


1. The cast: every nu/nu_e variable

name where it lives meaning
motion.nu_e / motion_cache.nu_e utils/robot.py:162 (twist("cam","local")) measured EE twist, EE-local frame R_te
des.nu_e Desired record field desired EE local twist (a side-copy for logging + guidance rollout)
self.nu_e.local.des breve_controller.py desired local twist used by control (the load-bearing twin of des.nu_e)
self.nu_e.local.prev breve_controller.py:106,122 previous desired local twist — vestige
self.nu_e.oplus.des breve_controller.py desired twist in the reduced (⊕/circumcentroidal) frame
self.nu_e.oplus.dot breve_controller.py desired reduced acceleration (RHS feedforward)
self.nu_e.oplus.prev breve_controller.py:607,648 previous reduced desired (for the FD acceleration)
st.nu_e_oplus com_controller.py, breve_core_controller.py the EE reduced-twist state (integrated)
nu_e_oplus_des (local var) breve_controller.py:619–648 the reduced desired twist, computed in-method
nu_e_oplus_des (diag field) breve_controller.py:521 a logged copy — include:false
nu_local, nu_oplus, nu_oplus_dot, nu_des guidance functions transient locals (FF construction)

2. The flow: guidance → control → wrench → state

GUIDANCE (EEGuidance / analytic_feedforward)            CONTROL (BreveController)
────────────────────────────────────────────           ─────────────────────────────────────────────
desired EE pose  des.p_e, des.R_e   ───────────────►   build_desired_for_step  (breve_controller.py:69–130)
                                                          ├─ analytic ON:  analytic_desired_nu_e_feedforward(st,des)  :94
desired EE twist, TWO constructions:                      │     → (nu_local, nu_oplus, nu_oplus_dot) in R_ed
  • analytic (ideal standoff, R_ed)  ee_guidance.py:267   │     → _set_analytic_oplus_feedforward(...)               :107
  • FD (committed-pose d/dt, R_te)   ee_guidance.py:225   └─ analytic OFF: desired_nu_e_feedforward_consistent(des,des_prev) :110
                                                                → update_desired_nu_e_oplus_feedforward(...)         :123
                                                          stores:  self.nu_e.local.des            (LOAD-BEARING)   :105/117
                                                                   self.nu_e.oplus.{des,dot}      (LOAD-BEARING)   :605/647
                                                                   des.nu_e                       (log + rollout)  :104/116/120

measured twist  motion.nu_e = twist("cam","local")  (R_te)  ─────────────────────────────────────────┐
                                                                                                       ▼
WRENCH:  nu_e_tilde() = motion.nu_e − nu_e.local.des            (local error)        :258  ───►  D-term in compute_omega_e_oplus :293
         nu_e_oplus_tilde() = ⊕(motion) − nu_e.oplus.des        (reduced error)      :609
            → nu_e_damping_error()  :242  → v_breve_damping_error()  (breve_core_controller.py:78)
            → RHS34b D_breve term   :410  → v_breve_dot  :463  → integrate
ACCEL FF: nu_e.oplus.dot  ───►  rhs_feedforward()  :351  (M_breve · [0; nu_e.oplus.dot])

STATE:   st.nu_e_oplus  ←  integrate nu_e_oplus_dot  (breve_core_controller.py:115)
         → reconstruct_generalized_velocity  :486  → Γ-solve → full generalized velocity → joint torques

LOG:     (motion.nu_e, des.nu_e) pair  (utils/data_classes.py:280)  → key tracking_error.arm.nu_e  [ILL-POSED METRIC]
         nu_e_oplus_des (diag)          (breve_controller.py:521)    → include:false                [DEAD LOG]

The two frames are the crux. The measured twist motion.nu_e lives in the realized EE frame R_te; the analytic desired twist lives in the desired standoff frame R_ed. Because axis_only_attitude zeros the commanded roll rate w_z (the camera is axisymmetric — boresight roll is irrelevant to what it sees), R_te and R_ed roll freely apart (~52°). The control path is fine — the D-term works in the local frame R_te (nu_e_tilde, line 258) and the reduced damping uses a consistent ⊕ map — but any metric that differences motion.nu_e against the analytic des.nu_e is comparing across those rolled frames. That is the ill-posedness (see §5).


3. LIVE / LOAD-BEARING (do NOT touch)

ast-grep caller traces ($_.fn($$$) and attribute-access patterns) confirm a live consumer for each:

symbol consumed by evidence
motion.nu_e nu_e_tilde (D-term), nu_e_oplus_tilde (reduced error) breve_controller.py:260,612
nu_e_tilde() D-term in compute_omega_e_oplus; x_e_tilde_dot :283, :252
nu_e_oplus_tilde() nu_e_damping_error :244
nu_e_damping_error() v_breve_damping_errorRHS34b breve_core_controller.py:78breve_controller.py:410
nu_e_local_to_oplus() reduced error, realized floor :611, :543, :621, :630
nu_e.local.des nu_e_tilde (260), ⊕ desired (622/631) read at :260,622,631
nu_e.oplus.des cruise-lag floor input (366), reduced error (619) read at :366,619
nu_e.oplus.dot RHS acceleration feedforward read at :351
st.nu_e_oplus integration + reconstruction breve_core_controller.py:115; com_controller.py:240,321; breve_controller.py:488
analytic FF / FD FF / both ⊕ updaters one control path each called at :94/110/107/123
des.nu_e (the field) guidance rollout + logging guidance_rollout.py:68 (des_prev.nu_e), ee_guidance.py:337 (fake_kinematics), data_classes.py:280,289

Takeaway: the EE twist nu_e, in all its working forms (measured, desired-local, desired-⊕, the state, the errors, the feedforward), is load-bearing. None of it is a deletion candidate.


4. DEAD / VESTIGIAL (safe to remove or already inert)

4a. self.nu_e.local.prev — genuinely DEAD

ast-grep run --pattern '$_.nu_e.local.prev' over GNC utils analysis validation returns only two nodes, both assignment LHS (writes), and zero reads:

GNC/breve_controller.py:106:   self.nu_e.local.prev = nu_local.copy()
GNC/breve_controller.py:122:   self.nu_e.local.prev = self.nu_e.local.des.copy()

It is a fossil from when the desired-twist finite-difference derivative was taken in the local frame. The design moved that derivative to the reduced (⊕) frame, where it now reads nu_e.oplus.prev at breve_controller.py:640. nu_e.local.prev was left behind, written but never consumed. Removable.

4b. nu_e_oplus_des diagnostic log line — DEAD LOG

controller_diagnostics logs nu_e_oplus_des=self.nu_e.oplus.des (breve_controller.py:521), but the catalog marks it include: false (metrics.yaml), so it is never serialized to the NPZ and no report/scoreboard reads it. (The same name nu_e_oplus_des as a local variable at :619–648 is load-bearing — different scope; only the line-521 diagnostic copy is dead.) Removable.

4c. self.nu_e.oplus.prev write at line 607 — conditionally vestigial (KEEP)

oplus.prev is read at :640 (the FD acceleration (des − prev)/dt), so the field is live in FD runs. But in the analytic path, _set_analytic_oplus_feedforward sets oplus.dot directly (:606) and the write at :607 is never read (the FD branch is not taken). It is harmless field-consistency, not cleanly removable (the FD path needs the field). Keep; note the asymmetry.


5. The nu_e tracking metric — ILL-POSED, not dead

The logged pair (motion.nu_e, des.nu_e) (data_classes.py:280) becomes the catalog key tracking_error.arm.nu_e, reduced to a per-step error actual − desired by data_analyzer. It is never read back by the controller (purely diagnostic), so it is not load-bearing — but it is not “dead” either; reports and plots consume it. The problem is that it is ill-posed in analytic runs:

This is exactly the “54°” — the geodesic frame angle ‖so3_log(R_ed^T R_te)‖ ≈ 53.8° (measured by the frame_angle_diag run) re-expresses the same world velocity in a 54°-rotated x–y split. The boresight pointing error over the same window is only ~2.2° (ratio 24.6×). The right way to judge EE tracking is the world-frame p_e and the pointing versine — both frame-invariant — not this metric. So the metric should be re-posed (point reports at p_e/pointing) or dropped; either way the EE twist nu_e itself stays.


6. The same roll contaminates the cruise-lag floor (why this matters beyond cleanup)

The floor formula (eq 4.14, cruise_lag_floor, analytic_feedforward.py:144) is fed v_breve_des = [omega_b; nu_e.oplus.des] and computes C_breve · v_breve_des as the Coriolis forcing. The analytic floor feeds nu_e.oplus.des built in R_ed; the realized floor feeds the measured twist mapped from R_te (breve_controller.py:540–548). Those are the same two rolled frames that make the nu_e metric ill-posed. So the boresight roll is not only a metric nuisance — it sits inside the floor’s forcing term and is part of why the “floor” reads 0.044 (analytic), 0.055 (realized), or 0.123 (FD/consistent, cross-run). The honest, frame-consistent number — eq 4.14 fed the committed-pose velocity d/dt(p_ed) the EE actually tracks, within one run — is the follow-up computed via the new floor_pe_consistent diagnostic. (Full floor narrative: cruise_lag_floor.md.)


7. Verdict