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

EE-twist orthogonality: frame/sign artifact or real tracking deficiency?

Generated: 2026-06-23 (EDT) · Run: logs/logs_Jun23_26/full_control_floor_an/run/npz/run_PM0523/full_control_floor_an.npz (analytic FF on, log_cruise_lag_floor on) Evidence script: validation/nu_e_orthogonality.py · Window: non-derated operational (_op, n=9066 steps) Question: Is the large logged EE-twist error nu_e (actual vs desired) a coordinate/sign artifact (⊕ frame vs local), or a real direction-tracking deficiency? Which DOF — linear or angular?


Answer first

Measured decomposition (operational window, medians)

DOF |actual| |desired| |error|
v_x 0.7162 0.6286 0.3938
v_y 0.4401 0.5905 0.4260
v_z 0.0514 0.0296 0.0213
w_x 0.0973 0.1265 0.0910
w_y 0.1579 0.1345 0.0868
w_z 0.0526 0.0000 0.0526
block ‖actual‖ ‖desired‖ ‖error‖ angle (deg)
full (6) 0.8698 0.8826 0.7915 53.67
linear (0:3) 0.8475 0.8630 0.7718 53.60
angular (3:6) 0.1941 0.1848 0.1767 54.59

Self-consistency: law of cosines on the linear block, cos θ = (0.847² + 0.863² − 0.772²)/(2·0.847·0.863) = 0.59 → 53.7°, matches the directly-computed 53.60°.


Scope


Flow summary

The nu_e metric is a paired (actual, desired) tracking error. The two channels are produced on different code paths but expressed in the same EE-local frame, then logged together and reduced by the catalog as a Euclidean ERROR.

flowchart TD
    subgraph ACTUAL [actual channel — plant]
      A1["robot.py:162  nu_e = twist('cam','local')"] --> A2["data_classes.py:237  KinematicsCache.nu_e = motion.nu_e"]
    end
    subgraph DESIRED [desired channel — guidance feedforward]
      D1["ee_guidance.py:238  v_des_world = (p_now - p_prev)/dt"] --> D2["ee_guidance.py:242/249  nu_des = R_te.T @ world  (same R_te)"]
      D2 --> D3["ee_guidance.py:252  axis_only -> nu_des[w_z]=0"]
    end
    A2 --> P["data_classes.py:280  pair: nu_e=(actual.nu_e, desired.nu_e)"]
    D3 --> P
    P --> C["metrics.yaml:96-99  variable: nu_e  (err-default, log_desired)"]
    C --> R["data_analyzer.py:1041/1048  ERROR = actual - desired"]
    R --> M["signal_measurement.series('nu_e') -> ||error|| per step"]

    OPLUS["breve_controller.py:580  nu_e_oplus_tilde (⊕-frame)\nNOT logged as nu_e; metrics.yaml:281-284 include:false"] -.separate quantity.-> P

Inconsistency the diagram exposes: the dashed ⊕-frame error is the one the damping law actually drives toward zero, yet it is not the nu_e the metric reports. So “the controller tracks ⊕-twist” and “logged local nu_e error is large” are not contradictory — they are two different numbers.


Key functions (call order)

  1. Robot motion buildutils/robot.py:162 nu_e = self.twist("cam","local"). The actual EE twist in the EE-local frame R_te (line 160–161 uses the lwa twist only to get the world-frame point velocity p_e_dot; nu_e itself stays local).
  2. EEGuidance.desired_nu_e_feedforward_consistentee_guidance.py:225. FD reference velocity: v_des_world=(p_now-p_prev)/dt (238) → nu_des[:3]=R_te.T@v_des_world (242), nu_des[3:]=R_te.T@ω_des_world (249), with R_te = actual EE frame (241). axis_only zeroes w_z (252). Stores des.nu_e (260).
  3. KinematicsCache.updatedata_classes.py:237 copies motion.nu_e into the actual cache that feeds logging.
  4. StepLog.pairdata_classes.py:266,280 groups nu_e=(actual.nu_e, desired.nu_e) for the tracking-error log.
  5. Catalog specmetrics.yaml:96-99 (variable: nu_e, inherits err-default at 25–36: log_desired: true, pointing: false).
  6. SignalAnalysis.actual_desired_values / paired_metric_signaldata_analyzer.py:1041,1048. Pairs the logged tuple and forms ERROR = actual − desired (non-pointing).
  7. (separate) BreveController.nu_e_oplus_tildebreve_controller.py:580, via nu_e_local_to_oplus (555). The ⊕-frame velocity error used by the damping RHS; logged only as nu_e_oplus_des (metrics.yaml:281-284, include:false), not as nu_e.

Findings

F1 — Frame/sign-artifact hypothesis is falsified (structural). Evidence: actual and desired nu_e are both R_te.T·(world) in the same actual EE frame (robot.py:162; ee_guidance.py:241,242,249); the pairing is (actual.nu_e, desired.nu_e) (data_classes.py:280); the ⊕-frame error is a distinct, non-nu_e quantity (breve_controller.py:580; metrics.yaml:281-284). Impact: the large nu_e error is a genuine physical residual, not a coordinate clash. Next step: none for the frame question — closed.

F2 — The residual is a ~54° direction offset with matched magnitude, linear-dominated. Evidence: validation/nu_e_orthogonality.py on run_PM0523 (n_op=9066): linear ‖actual‖ 0.847 ≈ ‖desired‖ 0.863, angle 53.6°; angular block 5× smaller. Law-of-cosines cross-check matches. Impact: the fix is not a speed/gain on the desired direction (that’s the magnitude axis, already matched) — it is the heading of the EE velocity. Confirms velocity_ff (magnitude-only, inert) is not the lever. Next step: identify what rotates the achieved EE velocity off the feedforward heading (F4).

F3 — “Nearly orthogonal” was an over-characterization; correct it in the record. Evidence: direct cosine = 53–55°; the prior CLAIM’s own norms (0.79/0.88/0.89) imply 64°, not 90°. Impact: prevents a downstream chase of a non-existent orthogonality. Next step: CLAIMS.md + GNC/INSIGHTS.md:62 corrected (done with this report).

F4 — Open question: is the offset feedback (symptom) or feedforward infidelity (cause)? (inference) Evidence: desired.nu_e is reference-only feedforward (ee_guidance.py:238); actual.nu_e is closed-loop. A heading offset is the expected closed-loop response to the standing p_e≈0.107 m lag. Impact: decides the lever. If the offset is the feedback correction toward the lagged target, the upstream cause is the position lag itself (and the cruise-lag floor premise v̆=v̆_des is violated because feedback is active) — not a twist-direction defect to “fix.” Next step: the distinguishing experiment in Proposed Next Tasks.


Proposed next tasks

TASK A — Split actual twist into feedforward + correction; test the correction’s heading

BODY: Form Δ = actual.nu_e − desired.nu_e (linear block) per step over _op, and the EE-frame position-error direction R_te.T·(p_e_actual − p_e_des). Compute the per-step angle between Δ and that position-error direction (and correlate/lead_lag of ‖Δ‖ vs p_e). If Δ points along the position error (small angle, positive correlation), the ~54° offset is the feedback closing the lag → nu_e error is a symptom of p_e, not an independent cause. Modify: none. Inspect-not-modify: breve_controller.py (EE damping RHS), ee_guidance.py. Create: extend validation/nu_e_orthogonality.py (add the Δ-vs-p_e-direction panel). Validation: angle distribution + Pearson/Cliff’s-delta via the signal_measurement facade; no sim re-run.

TASK B — Locate the EE position-loop authority and quantify the magnitude-matched / heading-off regime

BODY: Establish whether the EE has explicit position-proportional feedback or tracks position only through the velocity (⊕-twist) loop + pose schedule. The F2 signature (right speed, wrong heading) is the fingerprint of a velocity-loop-only architecture with a standing along-track/cross-track lag. Map the lever candidates (EE position stiffness vs feedforward heading fidelity) to code, excluding velocity_ff. Modify: none (report-only). Inspect-not-modify: breve_controller.py (all_control_terms, RHS), GNC/equations/current_sota.md §3–5. Create: a short follow-up section here or a new dated report. Validation: trace + cite; propose the eventual A/B as a separate run-spec.


Provenance