Generated: 2026-06-18, morning session (EDT) Mode: Investigator (report-only — no code was modified) Question: What are we actually doing about speed/impedance derating? How should we best implement it to be closer to the intended SPEED derating (i.e. within the guidance algorithm)? Companion note: GNC/equations/Inconsistencies_Note_Jun18.md Verification: every load-bearing claim below was independently re-read by an adversarial verifier (5/5 claims adjudicated; 2 of my prior framings were corrected — see Findings F4, F5).
γ(s_min_G) = scale_by_svd(s_min_G, conditioning.sigma) ∈ [floor, 1]
is computed from the smallest singular value of the coordinated Jacobian
Γ, then applied three structurally different ways: (1)
arm → gains (γ·K_arm, γ·D_arm), (2) base →
reference (γ·v_c, γ²·a_c, γ·ω_b), (3) base →
wrench (γ·τ_b^⊕, but logged only).nu_e produced in
ee_guidance.py is touched by γ nowhere —
only a fixed v_max safety clamp. So “Speed Derating through
Impedance” derates impedance, not speed, on the arm.
(ee_guidance.py:252, :304).compute_tau_b_oplus does run
every step (via super() dispatch) and its γ-scaled wrench
is logged, but the base attitude is integrated from the
separate RHS34b → v_breve_dot path, so γτ_b^⊕ does not
drive the base. The report describes a quantity that the live controller
computes and discards.EEGuidance extends
BaseGuidance, which owns
derate_desired_by_gamma. A true speed-derate scales the
EE desired twist by γ in guidance (mirroring the base),
and retires the arm gain-derate. The empirical D-gain
A/B (Jun 18) and the σ_crit,1 = v/x_max derivation both point this
way.name: base controller mode
imports BaseController from it (runner.py:104-108); you
ran base_only_frame_check.yaml through that path on Jun 17.
Retiring it requires relocating the still-live
BaseController and repointing the base
baseline + smokes — see Proposed Task 2.Question owner: the derating (“Pareto”) layer of singularity handling — explicitly not the deeper damped-inverse / cascade-floor layer (user: “Don’t go deeper into singularities”).
Inspected (in scope): - GNC/breve_controller.py — the
live coordinated controller. - GNC/guidance/ee_guidance.py
— the EE-guidance algorithm. - GNC/base_guidance.py — base/CoM
reference derate. - GNC/com_controller.py —
CC_Controller base class (the
all_control_terms super-body). - GNC/guidance/analytic_feedforward.py
— reduced_desired_twist (reference-only). - analysis/runner.py — the controller
factory (make_controller). - Run-spec / baseline YAMLs and
validation/test_baselines.py for the retirement dependency
map.
External boundaries (stopped, inferred from call
site): - utils/sampling.py:scale_by_svd and
sigma_min_and_cond — the γ ramp and the SVD of Γ. Behavior
taken from the call site: a monotone piecewise-linear ramp returning
floor below low, 1.0 above
high, linear between. - utils/robot.py —
assembly of Γ and dyn.s_min_G. -
utils/geometry.py:block9 — read only to confirm argument
order (base = upper-left 3×3, arm = lower-right 6×6).
Not modified: everything. This is a report.
γ is born once per step from the conditioning of Γ, then fans out to three consumers that each interpret it differently. The diagram is the whole story in one picture:
flowchart TD
SMG["dyn.s_min_G\n(smallest singular value of Γ)"] --> SBS["scale_by_svd(s_min_G, conditioning.sigma)\n= γ ∈ [floor, 1] (utils/sampling.py)"]
SBS --> ARM["ARM (controller)\narm_breve_gains: γ·K_arm, γ·D_arm\nbreve_controller.py:393-395"]
SBS --> BASEREF["BASE (guidance)\nderate_desired_by_gamma: v_c·γ, a_c·γ², ω_b·γ\nbase_guidance.py:135-142"]
SBS --> TAUB["BASE wrench (controller)\ncompute_tau_b_oplus: τ_b·γ\nbreve_controller.py:332-333"]
SBS --> WIND["ARM integral anti-windup\nfreeze if γ < scale_gate\nbreve_controller.py:243-245"]
ARM --> RHS["RHS34b: K_breve, D_breve\n(base gains UNSCALED)\nbreve_controller.py:437-439"]
BASEREF --> RHS
RHS --> VDOT["v_breve_dot = M_breve⁻¹·RHS\n→ ω_b_dot, ν_e_oplus_dot\nbreve_controller.py:521-524"]
VDOT --> STATE["live base + arm state\n(this is what flies)"]
TAUB --> GSTACK["G = [f_c, τ_b, ω_e_oplus]\nF = Γᵀ·G → LOGGED\ncom_controller.py:198-208"]
GSTACK -. "logged, NOT integrated\n(superseded by v_breve_dot)" .-> STATE
EE["EE desired twist ν_e\nee_guidance.py:252 / :304\n(NO γ — full speed)"] --> RHS
The two solid paths into STATE (arm gains + base
reference) are the live derate. The dashed path (γτ_b^⊕) is the report’s
described mechanism — it reaches the log, not the
integrator. The EE twist enters STATE at full
magnitude.
make_controller (runner.py:104-117) — the
factory. name: arm → BreveController +
EEGuidance (the live mission path); name: base
→ BreveController_L.BaseController; name: com
→ CC_Controller.BaseGuidance.build_..._for_step /
derate_desired_by_gamma (base_guidance.py:119-142)
— guidance scales the base reference by γ:
v_c *= γ; a_c *= γ²; ω_b *= γ. Gated on
controller.base.conditioning.enable. Reads
dyn.s_min_G — guidance already holds
dyn.blend_base_goal_by_conditioning (base_guidance.py:146-160)
— a second reference-side base move: blends the desired base
x-axis toward inward and scales ω_b by γ near
singularity.EEGuidance.desired_nu_e_feedforward_consistent
(FD) (ee_guidance.py:218-253)
and analytic_desired_nu_e_feedforward
(closed-form) (ee_guidance.py:267-305)
— produce the EE desired twist. Only a fixed v_max clamp;
no γ.CC_Controller.all_control_terms (com_controller.py:190-221)
— the super-body every controller runs. Calls
self.compute_tau_b_oplus(des) (dynamic dispatch → the
override), stacks G = [f_c, τ_b, ω_e_oplus], maps
F = Γᵀ·G, and logs
forces.tau_b_oplus = τ_b.BreveController.compute_tau_b_oplus
(breve_controller.py:326-334)
— the override. τ_b *= conditioning_scale(s_min_G) when
conditioning is on. This is the report’s γτ_b^⊕.conditioning_scale /
arm_gain_scale / arm_breve_gains (breve_controller.py:313-322,
:393-395) —
conditioning_scale = scale_by_svd(s_min_G, base.conditioning.sigma);
arm_breve_gains returns
γ·g.arm.D, γ·g.arm.K.update_x_e_integral (breve_controller.py:240-254)
— the EE integral. Freezes (return early) when
conditioning_scale(s_min_G) < integral.scale_gate. A
fourth consumer of γ.RHS34b (breve_controller.py:433-464)
— assembles D_breve = block9(g.base.D, D_arm),
K_breve = block9(g.base.K, K_arm). Base gains raw;
arm gains γ-scaled. The EE reference
nu_e.oplus.des enters here (velocity-FF and
K_breve @ x_tilde) unscaled.v_breve_dot → all_control_terms
(override) → _apply_step_state_update (breve_controller.py:506-533)
— solves M_breve · v̇ = RHS34b, splits into
ω_b_dot (base) and ν_e_oplus_dot (arm),
integrates the state. This is what flies — and it
ignores the γτ_b^⊕ wrench from step 6.| Frame | γ multiplies | What is preserved | Physical effect | Evidence |
|---|---|---|---|---|
| Arm | the gains K_arm, D_arm |
the EE reference (full speed) | softer impedance; arm still wants full velocity | breve_controller.py:393-395, :437-439 |
| Base | the reference v_c, a_c, ω_b |
the base gains (full stiffness) | slower commanded motion; base holds firm | base_guidance.py:135-142; base gains raw at :438-439 |
These are not two flavors of one policy; they are opposite
control-design choices applied to the two halves of the same
plant. Verified: base-gains-unscaled confirmed,
arm-gains-scaled confirmed.
The report section is titled “Speed Derating through
Impedance,” but on the arm the only thing γ touches is K and
D (impedance). The EE desired twist nu_e — the
actual commanded speed — is produced with no γ anywhere (ee_guidance.py:218-305)
and enters RHS34b raw (breve_controller.py:450-457).
Verified: ee-twist-underated confirmed — a full regex
sweep of ee_guidance.py for
gamma|derate|s_min_G|scale_by_svd|conditioning returns
nothing but a placeholder sigma_min=1.0.
This is the crux. Gain-derate changes the closed-loop
impedance (how hard the loop fights error — a
dynamics/stability change). Speed-derate changes the
commanded trajectory (how fast you ask for motion — a
reference change that leaves stability untouched). σ_crit,1 =
v/x_max is a bound on commanded velocity, so the
derivation justifies the second. The code applies the
first to the arm — which is why the Jun 18 D-gain A/B saw the
floor collapse (softer K/D let the arm overshoot into the cusp:
min s_min_J 0.0255 → 0.0000).
flowchart LR
CTB["compute_tau_b_oplus\nτ_b *= γ\nbreve_controller.py:332-333"] --> LOG["forces.tau_b_oplus (LOG)\ncom_controller.py:204"]
CTB --> F["F = Γᵀ·[f_c, τ_b, ω_e]\n(logged generalized force)"]
RHS2["RHS34b → v_breve_dot\nbreve_controller.py:521-524"] --> OMB["ω_b_dot integrates base state"]
LOG -. "report says THIS drives the base" .-> OMB
style LOG stroke-dasharray: 5 5
The report says the base derate is τ_b^⊕ → γτ_b^⊕. The
live code does compute that (breve_controller.py:332-333)
and log it, but the base attitude integrates from ω_b_dot
(out of RHS34b), so γτ_b^⊕ is
computed-but-inert for the live base state. Meanwhile the
actual base derate (reference-scaling in guidance,
Inconsistency 1) is undocumented in that report
section. So the section is wrong twice: it names an inert mechanism and
omits the live one. Verified: tau-b-oplus-dead-live was
refuted as literally worded (the method is called every
step via super()) — the correct statement is “called and
logged, but superseded for integration.”
arm_gain_scale gates on
base_cfg.conditioning.enable (breve_controller.py:320)
and conditioning_scale reads
ctrl_cfg.base.conditioning.sigma (:315). So a single
base-side switch silently controls: (a) the arm
gain-derate, (b) the base reference-derate,
(c) the logged γτ_b^⊕, (d) the EE
integral anti-windup freeze (:243). A reader toggling
base.conditioning would not expect the arm
impedance and the arm integrator to move. (Sub-note: the
windup gate at :243 calls conditioning_scale
unconditionally — it does not re-check the
enable flag — so it computes a real γ even paths that
nominally read “off.” Flagged for the change, F6.)
Principle: to “derate the speed within the guidance
algorithm,” scale the commanded task velocity by γ
where it is finalized in guidance — exactly as the base already does in
derate_desired_by_gamma.
EEGuidance(BaseGuidance) already inherits that method (ee_guidance.py:82), and
guidance already receives dyn (hence
dyn.s_min_G) at the base-desired step (base_guidance.py:124). So
this is a symmetric mirror, not a new subsystem.
Injection sites (verified): - FD
path: ee_guidance.py:252 —
des.nu_e = nu_des.copy(). Scale
nu_des *= γ(s_min_G) before storing. - Analytic
path: ee_guidance.py:304-305
— scale nu_local and the returned
nu_oplus, nu_oplus_dot by γ (a partial scaling
would desync the velocity-FF and accel-FF and shift the L1 equilibrium
that RHS34b:447-453 constructs). - Then retire the
arm gain-derate: make arm_gain_scale return
1.0 (or strip γ from arm_breve_gains, breve_controller.py:393-395),
so the arm keeps full stiffness.
Three caveats that make this “meaty,” not a
one-liner (each is a Finding below): 1.
Plumbing (modest) — the EE-twist methods don’t
currently take dyn/s_min_G; thread the same
dyn guidance already holds into them (F3). 2.
Anti-windup coupling (must change jointly) —
the integral freeze at :243 uses γ as a proxy for “gains
were shrunk.” If the gains no longer shrink, the freeze either fires
spuriously (full-gain integrator frozen) or, if γ is also zeroed there,
never fires (windup unguarded). The two uses of
conditioning_scale cannot be changed independently
(F6). 3. Feedforward purity (design
choice) — analytic_feedforward.py is deliberately “a
function of the reference alone, never of the measured state” (analytic_feedforward.py:1-8).
s_min_G is a function of the measured
q. Scaling the analytic FF by a measured-state γ breaks
that purity. Decide: a reference-evaluated s_min_G (γ at
the desired pose) preserves purity; a measured-state γ is
simpler but couples the FF to the plant (F7).
F1 — The arm derates impedance, the base derates reference (mirror-opposite). Evidence: breve_controller.py:393-395 (γ·gains) vs base_guidance.py:135-142 (γ·reference); base gains raw at :437-439. Both confirmed by adversarial re-read. Impact: indefensible asymmetry; the two halves of the plant follow opposite derate policies under one name. Next step: unify on reference/speed-derate (Proposed Task 1).
F2 — The arm’s commanded EE speed is never derated.
Evidence: ee_guidance.py:218-305
(no γ; only v_max); reference enters RHS34b
raw :450-457.
Impact: “Speed Derating through Impedance” derates impedance,
not speed, on the arm — the opposite of the σ_crit,1 = v/x_max intent;
corroborated by the Jun 18 D-gain A/B (softer arm D worsened the floor).
Next step: inject γ on nu_e (pinpoint above); A/B
vs the current gain-derate (Proposed Task 1).
F3 — EEGuidance already inherits
derate_desired_by_gamma and already has access to
dyn. Evidence: ee_guidance.py:82
(class EEGuidance(BaseGuidance)); base_guidance.py:124
(sigma_gamma = dyn.s_min_G). Impact: the symmetric
fix is low-friction — the same γ source and the same idiom the base
uses. Next step: a derate_twist_by_gamma sibling
that scales nu_e (+ nu_oplus,
nu_oplus_dot).
F4 — compute_tau_b_oplus is called every step,
but its γτ_b^⊕ is logged, not integrated. (corrects my
prior “dead method” framing) Evidence:
BreveController.all_control_terms → super()
(breve_controller.py:518-519)
→ CC_Controller.all_control_terms calls
self.compute_tau_b_oplus(des) (com_controller.py:195),
dispatching to the override at :326. The result lands in
forces.tau_b_oplus (com_controller.py:204); base
state integrates from v_breve_dot (breve_controller.py:521-524).
Impact: the report’s γτ_b^⊕ is real code that does not drive
the live base; the actual base derate is the (undocumented)
reference-scaling. Two-way drift. Next step: reconcile the
report’s §“Speed Derating” with the reference-derate reality; decide
whether the logged γτ_b^⊕ should be removed when
BreveController_L is retired (it survives as a logged force
field only).
F5 — BreveController_L is NOT safe to delete
today; the live base mode depends on it.
(corrects “retire it today” as a free delete)
Evidence: make_controller
case "base": from BreveController_L import BaseController
(runner.py:104-108);
BaseController/EEController are defined
only in BreveController_L.py (:43, :259). Live, non-test
consumers of name: base: the base pinned
baseline (test_baselines.py:12),
base_only_frame_check.yaml (your Jun 17 frame A/B),
dispatch_smoke.yaml, determinism_smoke.yaml.
Impact: deleting the file breaks the production base-only
controller path and three live run-specs + a pinned baseline. The legacy
arm class (EEController) is genuinely retire-able;
the base-only BaseController is not — it must be relocated,
not deleted. Next step: Proposed Task 2 (extract
BaseController, repoint, re-pin).
F6 — The EE integral anti-windup is coupled to γ and gates
unconditionally. Evidence: breve_controller.py:240-245
—
scale = self.conditioning_scale(self.dyn.s_min_G); if scale < scale_gate: return
(no enable re-check). Impact: changing the arm
from gain-derate to speed-derate decouples the freeze from the mechanism
it guards; it cannot be changed in isolation. Also worth confirming
integral.scale_gate vs floor=0.25: if
scale_gate ≤ floor the freeze can never fire (guard inert).
Next step: fold the windup decision into the speed-derate
design; read arm_cfg.integral.scale_gate and decide the new
gate semantics.
F7 — Scaling the analytic feedforward by a measured-state γ
breaks its reference-only purity. Evidence: analytic_feedforward.py:1-8
(reference-only by design); s_min_G is a function of
measured q (base_guidance.py:124).
Impact: a design fork — measured-state γ (simple, couples FF to
plant) vs reference-pose γ (preserves purity, more work). Next
step: user decision before the analytic-path injection (the FD path
has no such purity claim).
F8 (peripheral, do-not-chase) — The shipped conditioning
thresholds are not cascade-consistent. Evidence: live
conditioning.sigma = {low: 0.005, high: 0.025, floor: 0.25}
(parameters.yaml:87-92);
the report’s cascade wants high = σ_crit,1 and
low = σ_crit,1² — but 0.025 ≠ 0.005^0.5 and
the report’s σ_crit,1 ≈ 0.143. The critical values are
implicit (never stored as config; appear only as
comments in cascade-sweep analysis YAMLs). Impact: belongs to
the deeper cascade-floor thread the user is running in parallel
— flagged, not investigated here (out of the Pareto-layer scope).
Next step: leave to the σ_crit cascade track.
TITLE: Mirror the base’s reference-derate onto the arm; A/B it against the incumbent gain-derate on the full helix.
BODY: Add a guidance-side EE-twist derate
(nu_e *= γ, plus
nu_oplus/nu_oplus_dot on the analytic path)
behind a default-OFF flag, and a switch that turns the arm gain-derate
OFF. Run a 3-way comparison on the corrected base
(omega_body_frame=true), targeting-OFF, full helix,
dispatched across the two x86 boxes — reusing the exact
dgain_study rig (same SMIN mask,
pe_p99/pe_med/min s_min_J/frac_below/tau rms/z_b rms):
(A) incumbent gain-derate, (B)
speed-derate only, (C) neither (conditioning off, as a
floor reference). Pre-register the win: speed-derate holds or
lifts min s_min_J and does not
worsen pe_p99 vs incumbent. The hypothesis (from
F2 + the D-gain A/B) is that B holds the floor that A degrades.
Modify: GNC/guidance/ee_guidance.py
(the two injection sites), GNC/breve_controller.py
(arm_gain_scale switch + the windup gate per F6),
YAMLs_by_domain/parameters.yaml (two default-OFF flags,
byte-identical no-op). Inspect-not-modify:
GNC/base_guidance.py (derate_desired_by_gamma
as the template), analysis/dgain_study.py (the rig), the 5
validators. Create:
analysis/analysis_YAMLs/speed_derate_{gain,speed,off}.yaml;
analysis/speed_derate_study.py (facade-only verdict,
dgain_study clone); a CLAIMS entry.
Validation: default-OFF flags ⇒ the 5 baselines stay
byte-identical (no re-pin) and the determinism gate stays green; the new
flag’s effect shows in a mode/γ histogram (verify it actually engaged,
per the “verify provenance” memory).
TITLE: Split BreveController_L: delete
the superseded arm class, relocate the still-live base class.
BODY: BreveController_L.py holds two
classes with opposite fates. EEController (the legacy
two-class arm controller) is superseded by the flattened
BreveController (the byte-identical flatten A/B already
served its purpose, and the two have since diverged —
decisions.md:30), so it and its two legacy validators
retire. BaseController (the base-only diagnostic controller
behind name: base) is still live (frame
A/B, smokes, the base baseline) and must be
relocated, not deleted. Suggested: extract
BaseController into GNC/base_controller.py
unchanged, repoint runner.py:106, delete the rest of
BreveController_L.py. Because the class body is unchanged,
the base baseline should re-pin byte-identically (confirm,
do not assume).
Modify: analysis/runner.py:106 (repoint
case "base"); after relocation, delete
GNC/BreveController_L.py; remove the now-dead
base_window keyword from
EEGuidance.add_ee_goal (ee_guidance.py:202-209)
and its live call site (breve_controller.py:108-116);
scrub the BreveController_L docstring/comment mentions.
Inspect-not-modify:
validation/test_baselines.py,
validation/validate_base_baseline.py,
analysis/analysis_YAMLs/{base_only_frame_check,dispatch_smoke,determinism_smoke,baseline_base}.yaml.
Create: GNC/base_controller.py (relocated
BaseController); delete
validation/legacy/validate_breve_streamlined_ab.py and
validation/legacy/validate_base_refactor_ab.py (confirm no
conftest/pytest path collects
validation/legacy/ first). Validation:
re-run the 5 pinned baselines (the base one must stay
max_abs_diff = 0.000e+00 after the move);
python run_spec.py base_only_frame_check and the two smokes
must still execute the relocated BaseController.
Sequencing: Task 1 is the science (and is independent of the cleanup). Task 2 is house-cleaning and can run in parallel or after, on a fresh branch (the user already OK’d a new branch). Neither depends on the deeper σ_crit cascade work (F8).
This investigation traced six γ-threads in parallel, then ran a five-claim adversarial verification panel (each verifier re-read the source independently rather than trusting the trace). Outcomes:
| Claim | Verdict | Note |
|---|---|---|
base gains unscaled in RHS34b |
confirmed | base = upper-left block9, raw |
| arm gains γ-scaled, arm reference unscaled | confirmed | impedance change, not speed |
compute_tau_b_oplus dead in live path |
refuted | called every step via super(); result logged,
not integrated (F4) |
| EE desired twist underated | confirmed | only a fixed v_max; inject at
:252/:304-305 |
BreveController_L safe to delete (tests only) |
refuted | live name: base mode depends on it (F5) |
The two refutations are why the report is more defensible than the opening note: the γτ_b^⊕ finding is sharper (“computed-but-inert” beats “uncalled”), and the retirement is now scoped to its real live dependency instead of an over-confident delete.