Date: Jun 12, 2026 (CHAIN_13 task D4; pre-registered
bar: tasks/chain13_criteria.md §D4).
Question: the controller’s singularity-protection stack
uses four thresholds — 0.005, 0.02, 0.025, 0.05 — on one signal, the
smallest singular value σ₆ of the arm’s task map. Until now the defense
of these numbers was “they worked.” This document replaces that with
arithmetic a committee can audit: where 0.025 comes from, which
thresholds follow from a single stated budget, which one does not, and
what the layer ordering should be.
regen:
/Users/antoniahoffman/miniforge3/envs/new-pin-env/bin/python validation/threshold_sizing.py
(every number below is printed by that script; constants are commented
there with file:line sources).
Verdict up front (against the D4 bar): PASS on both prongs. (i) 0.025 reproduces within stated assumptions — but only against the simulation’s own joint-rate budget (the controller’s explicit ±50 rad/s velocity clip), at the 0.90 m/s cruise demand, with an identified margin of 1.39 ≈ √2. Against the real UR3 datasheet limits the same derivation gives a floor of 0.14–0.57, so 0.025 is a numerics floor, not a hardware floor — stated plainly in §5. (ii) The four-threshold spread reproduces as a family from that one budget — except the Γ-Tikhonov ε = 0.05, the single misfit — so §7 proposes a harmonized set (ε_Γ: 0.05 → 0.025) with the migration cost named, adoption explicitly gated on D2’s attribution verdict.
Vocabulary (derate, DLS, singularity-consistent handling, σ
engagement bands) is in wiki/terminology.md; everything
else is defined where it appears.
All four layers key on σ₆ — the smallest singular value of the arm’s
task map J⁺ (or of the full coordinated map Γ; the two are statistically
interchangeable here, Spearman ≥ 0.997 and identical derate decisions,
sming_vs_sminj_study.md). Sorted by engagement threshold,
descending:
| σ₆ threshold | layer | mechanism | source |
|---|---|---|---|
| 0.049 | Γ-velocity-solve Tikhonov ramps up | damping (DLS) | GNC/breve_controller.py:498-516;
parameters.yaml:75-80 |
| 0.025 | derate ramp starts (1.0 → 0.25 at 0.005) | slowdown (SC) | utils/sampling.py:70-78 via
GNC/base_guidance.py:124;
parameters.yaml:84-87 |
| 0.025 | kernel freeze (7-DOF only) | hard switch | utils/robot.py:312-323;
parameters.yaml:125 |
| 0.02 | damped J⁺ inverse starts | damping (DLS) | utils/robot.py:188-194;
parameters.yaml:132-137 |
| 0.005 | derate saturates at scale 0.25 | slowdown floor | utils/sampling.py:74-75 |
| 0.005 | hold-last-healthy J⁺ inverse | hard switch | utils/robot.py:185-186 |
Two notational facts verified against the code (they matter for the math):
lam = max(1e-4, 0.05² − σ²) directly
to ΓᵀΓ (breve_controller.py:508-510), so the code’s
lam is λ² in the literature’s notation.
The “engages below σ ≈ 0.049” onset is where the variable part exceeds
the constant floor: √(0.05² − 10⁻⁴) = 0.04899.λ = max(1e-4, 0.02 − σ) inside
σ/(σ² + λ²) (robot.py:204-210) — there λ
is the damping factor, ramping linearly from 10⁻⁴ at
the soft floor to 0.015 at the hard floor.Write the task map’s singular value decomposition J = Σᵢ σᵢ uᵢvᵢᵀ. The exact (pseudo-)inverse solution to J q̇ = ẋ is
\[\dot{q} \;=\; \sum_{i} \frac{u_i^{T}\dot{x}}{\sigma_i}\, v_i ,\]
so the component of the commanded task velocity that lies along the weakest output direction u₆ (the “dying direction”) is amplified by 1/σ₆ into joint rate. Worst case — the demand fully aligned with the dying direction —
\[\|\dot{q}\| \;\le\; \frac{v}{\sigma_6}, \qquad v = \text{commanded task speed [m/s]}.\]
Chiaverini’s sizing recipe inverts this: pick the joint rate you can tolerate, q̇_tol, and size the singular-region threshold
\[\boxed{\;\varepsilon \;=\; \frac{v}{\dot{q}_{\text{tol}}}\;}\]
His own worked example (“for the sake of argument”, §VI of the
paper): ε = 0.01 means a 0.1 m/s command along the dying direction gives
a 10 rad/s joint-velocity norm — i.e., he accepted q̇_tol = 10 rad/s and
sized ε accordingly. Holt & Desrochers state the identical principle
as prose: “The selection of σ_min should be based on the desired upper
bound on the norm of Δθ_d, which in turn is dictated by the saturation
limits of the joint-level controller” (NASA N94-26281, §6.1;
note this report is staged under the citekey
torres1992minimizing — cite it as Holt & Desrochers,
never as Torres–Dubowsky, per the study’s §7 source-hygiene alert).
Units: σ₆ of our task map carries m/rad on its translation rows, so v [m/s] / σ₆ [m/rad] = rad/s is dimensionally consistent. We use the translational reading throughout (the conservative one for this arm: the extension wall kills a translation direction).
The simulation enforces no joint rate limits.
Verified in both MuJoCo models: there are no joint velocity limits, and
every arm actuator is declared ctrllimited="false"
(models/ur3/ur3_box_limited_with_capsules.xml:124-129,
models/ur3/ur3_7dof_with_capsules.xml:141-147). The only
rate bounds in the loop are the controller’s own
guards: the ±50 rad/s clip on the solved generalized velocity
(breve_controller.py:511-513,
parameters.yaml:78) and the inverse-norm cap ‖J⁻¹‖ ≤ 1000
(parameters.yaml:137).
The real UR3 does enforce limits (Universal Robots
datasheet): base, shoulder and elbow at 180°/s = π rad/s; the three
wrists at 360°/s = 2π rad/s. At the singularity that actually binds us —
the full-extension wall (the reach factor of det J,
mission_1_success.md) — the dying direction is radial
end-effector translation, which is produced by the shoulder-lift and
elbow joints. Those are the slow joints, so π rad/s is
the binding hardware number.
So the honest statement, for the defense: in simulation the budget the thresholds must respect is Q = 50 rad/s (the code’s own clip — chosen, not physical); on hardware the budget would be π rad/s, sixteen times tighter, and the answer changes by an order of magnitude (§5).
ε_min = v / q̇_max, worst-case alignment. Mission speeds: 0.90 m/s is
both the orbit cruise speed (parameters.yaml:153, the Phase
00 matched point) and the cap on the commanded end-effector twist
(parameters.yaml:116); 0.45 m/s is the 7-DOF record-run
speed.
| v [m/s] | limit | q̇_max [rad/s] | ε_min | ε with margin 2 | 0.025 / ε_min |
|---|---|---|---|---|---|
| 0.90 | UR3 slow joints | π = 3.14 | 0.286 | 0.573 | 0.09× (floor 11.5× too low) |
| 0.90 | UR3 wrists | 2π = 6.28 | 0.143 | 0.286 | 0.17× |
| 0.90 | sim clip Q | 50 | 0.018 | 0.036 | 1.39× (floor sits inside [1×, 2×]) |
| 0.90 | Chiaverini’s q̇_tol | 10 | 0.090 | 0.180 | 0.28× |
| 0.45 | UR3 slow joints | π | 0.143 | 0.286 | 0.17× |
| 0.45 | UR3 wrists | 2π | 0.072 | 0.143 | 0.35× |
| 0.45 | sim clip Q | 50 | 0.009 | 0.018 | 2.78× (conservative) |
| 0.45 | Chiaverini’s q̇_tol | 10 | 0.045 | 0.090 | 0.56× |
Amplification admitted at our floor (undamped 1/σ at σ₆ = 0.025):
Assumption set (stated): budget Q = 50 rad/s (the controller’s own clip — the only rate bound that exists in the sim); reference demand v_ref = 0.90 m/s (cruise = twist cap); worst-case alignment of the demand with the dying direction.
Under those assumptions the thresholds fall out of the single inequality v·g(σ) ≤ Q, where g is each layer’s dying-direction gain:
Hardware read-across (the thesis-defense sentence). Against the real UR3’s π rad/s with a factor-2 margin, the floor that protects 0.90 m/s is ε = 0.57 and for 0.45 m/s it is 0.29 — an order of magnitude above 0.025. Conversely, the speed that 0.025 protects on hardware is v ≤ 0.025·π/2 ≈ 0.04 m/s (0.08 m/s with no margin). So: 0.025 is a simulation-numerics floor, sized (consistently) to the solver’s internal 50 rad/s budget; it is not a hardware joint-rate floor. If this controller ever drives a physical UR3, either the floor rises to ~0.14–0.57 or the near-wall speed falls to centimeters per second — the floor and the cruise speed are co-designed quantities, which is exactly Holt & Desrochers’ point in §2. One mitigating unknown, stated rather than assumed away: these are worst-case alignment numbers. The actual fraction of the demand along the dying direction during our entries is measurable offline from the logs (a D2-adjacent follow-up) and would relax the hardware numbers proportionally.
The pre-registered concern: the Γ-Tikhonov engages at σ ≈ 0.049, above the derate’s 0.025 — in the band [0.025, 0.049) the velocity solve is already damped while the vehicle has not slowed.
How bad is the band, quantitatively? On the engaged branch, lam = 0.05² − σ², so the Tikhonov filter factor (the fraction of the dying-direction demand the solve keeps) is
\[f(\sigma) \;=\; \frac{\sigma^2}{\sigma^2 + \text{lam}} \;=\; \Big(\frac{\sigma}{0.05}\Big)^{2},\]
i.e. the discarded fraction is 1 − (σ/0.05)²: 4% at 0.049, 36% at 0.040, 64% at 0.030, and 75% at 0.025 — the solve is throwing away three quarters of the dying-direction demand at the very moment the derate first begins to slow the vehicle. Discarded demand is not free: by Chiaverini’s eq. (14) the damping injects reconstruction error λ²/(σᵢ² + λ²) into every direction, and a demand component the solve cannot realize becomes tracking error in task space. (Identification worth recording: our Γ schedule lam = max(10⁻⁴, 0.05² − σ²) is exactly Chiaverini’s variable-damping law, his eq. (15), with ε = λ_max = 0.05, plus a constant β² = 10⁻⁴ isotropic floor as in his eq. (20). The layer is textbook — only its ε is out of family.)
The principle says: predictive slowdown first, damping as backstop. The published cost taxonomy (Sone & Nenchev’s SC-vs-DLS comparison, study §4.3) is one sentence: slowing the demand costs only time (schedule lag — the demand stays exactly realizable); damping costs tracking error in task space (speed and direction corrupted). A predictive layer should therefore spend the cheap currency first. The stack already does this on the J⁺ side (derate 0.025 > soft floor 0.02 — and §5.4 showed the derate is precisely what keeps the J⁺ layer inside budget). The Γ layer is the inversion of that ordering.
The case for the current ordering, stated fairly:
Resolution: this is an empirical question we have already
pre-registered. D2’s “damping-implicated” rule
(tasks/chain13_criteria.md) tests precisely whether band B1
(Tikhonov active, nothing else) carries an outsized share of in-window
pointing-error mass. The derivation’s contribution is the prior: the
band’s suppression is large near its bottom (75%), textbook-shaped, and
2.5× more conservative than the rate budget requires — so if D2
fires, the fix is principled and already sized (§7); if D2 does not
fire, the band is empirically harmless and the current ordering stands
at zero cost.
If a change is ever warranted, the family that makes all four layers consistent with the one budget (Q = 50 rad/s, v_ref = 0.90 m/s) is:
| knob | current | harmonized | rationale |
|---|---|---|---|
Γ-Tikhonov ε (gamma_regularization.floor) |
0.05 | 0.025 | onset drops to √(0.025² − 10⁻⁴) ≈ 0.023: nothing damps before the vehicle has begun to slow; gain cap becomes 1/ε = 40 ≤ Q; ordering becomes monotone slow → damp → hold |
| derate high | 0.025 | 0.025 (keep) | = √2 · v_ref/Q (§5.2) |
| J⁺ soft floor | 0.02 | 0.02 (keep) | = 1/Q exactly (§5.1) |
| derate low / J⁺ hard floor | 0.005 | 0.005 (keep) | = f·v_ref/Q within 10% (§5.3) |
| freeze floor (7-DOF) | 0.025 | 0.025 (keep) | rides the derate onset; it is a basis-continuity guard, conditioning-neutral |
Migration cost (real, and why we do not move now).
Changing ε_Γ alters the velocity solve on every step with σ₆
< 0.049 — for the saturated 6-DOF incumbent that is most of the
mission. Every trajectory diverges from the first affected step, so all
five byte-identical validators break by construction,
forcing a deliberate re-pin of the pinned baselines
(m7_on_s45_freeze.npz, the 8 Phase 00 npz as comparison
points) — and the standing rule is at most ONE deliberate re-pin per
chain (pre_risk.md §3b), each requiring full-helix A/B plus
±2% probes first. The D2 band definitions (the B0/B1 edge at 0.049) move
with it, breaking cross-chain comparability of every band statistic, and
the CHAIN_10 forensics tables become historical record. That is a large
bill to pay against an unquantified harm.
Recommendation: record the harmonized set here as the
principled target; adopt only if D2’s damping-implicated rule fires,
riding the already-registered directional-filtering follow-up with its
own A/B and the chain’s single re-pin. No code or config changes in this
chain (per the D4 bar).
Their warning is two-sided: a threshold too small lets the commanded step drive the arm through the singularity and oscillate between configuration branches (“chattering”, their §8); too large wastes tracking and speed — at their σ_min = 0.1 the control near the workspace boundary went weak a full 30° from the singular point, and their conclusion 3 concedes “unavoidable tracking error in the forbidden directions.” (Absolute σ values do not transfer between robots — different scales, different units — so we place ourselves by measured behavior, not by comparing 0.025 to their 0.1.)
Placement, in one sentence: 0.025 sits at the budget-consistent point for the simulation (§5), pays its “too-large” cost only where geometry forces it, and exhibits the “too-small” risk only as switch transients at the band edges — which is an argument about switch smoothness (L4), not about the value 0.025.
parameters.yaml:78), not physics. The family is internally
consistent with it; the hardware columns of §4 show what physics would
say.parameters.yaml:153) and the commanded-twist cap
(parameters.yaml:116) sharing that value.docs/Most_Relevant/papers/chiaverini1997singularity/ (1/ε
sizing §VI; variable damping eq. 15; DLS error eq. 14; isotropic floor
eq. 20).docs/Most_Relevant/papers/torres1992minimizing/
(mis-staged citekey; do not cite as Torres–Dubowsky —
singularities.md §7). σ_min selection §6.1; chattering and
forbidden-direction error §8 and conclusion 3.generated_reports/docs/research/singularities.md (§2
four-layer audit, §4.3 threshold sizing);
sming_vs_sminj_study.md;
phase00_round1_verdict.md (derate fractions, dwell cliff);
CHAIN_10 verdict (entries 23/14/2, thaw jump 82.9°);
generated_reports/GNC/Jun10_26/mission_1_success.md
(extension-wall factorization).regen:
/Users/antoniahoffman/miniforge3/envs/new-pin-env/bin/python validation/threshold_sizing.py