Skip to content

fix(safety): keep the velocity clamp live after another joint's violation - #303

Open
eastagiletracker wants to merge 1 commit into
FastCrest:mainfrom
eastagiletracker:agile-board/guard-velocity-clamp-skip
Open

fix(safety): keep the velocity clamp live after another joint's violation#303
eastagiletracker wants to merge 1 commit into
FastCrest:mainfrom
eastagiletracker:agile-board/guard-velocity-clamp-skip

Conversation

@eastagiletracker

Copy link
Copy Markdown

This PR proposes scoping the velocity gate in ActionGuard.check_single() to the joint being checked, so a position or effort violation on one joint no longer switches velocity clamping off for every joint after it — fixes #277. We include this PR work along with a full history of your repo at https://eastagiletracker.com/projects/323. You can sign in with your GitHub ID to claim ownership of the project.

What's wrong

In src/tether/safety/guard.py, the per-joint loop in check_single() gates the velocity check on not any("velocity limit" not in v for v in violations), which is all("velocity limit" in v for v in violations). violations accumulates across the whole loop, so the moment any joint records a position or effort violation the gate reads false for that joint and for every higher-indexed joint after it — their velocity deltas are then neither reported nor clamped. The clamp fails open in exactly the situation it exists for: an action that is already out of range.

Reproducing it on current main (97d2a87)

Public API, no hardware, no model:

import numpy as np
from tether.safety import ActionGuard, SafetyLimits

guard = ActionGuard(limits=SafetyLimits.default(num_joints=6), mode="clamp")
prev = np.zeros(6, dtype=np.float32)
# joint 0 out of position range; joint 3 jumps 3.0 against velocity_max 2.0
act = np.array([5.0, 0.0, 0.0, 3.0, 0.0, 0.0], dtype=np.float32)
print(guard.check_single(act, previous_action=prev).safe_action[3])

On main this prints 3.0 — joint 3's delta of 3.0 goes straight through the 2.0 cap, and the returned violations mention joint 0 only. Change the single unrelated value 5.0 back to 0.0 and the identical joint 3 delta clamps to 2.0. With this PR, both print 2.0.

The change

check_single() now records joint_violation_start = len(violations) at the top of each joint's iteration and gates the velocity check on len(violations) == joint_violation_start — "this joint has recorded nothing of its own yet" — instead of scanning the shared cross-joint list. That is four lines in src/tether/safety/guard.py. Same-joint precedence is deliberately unchanged: a joint that was itself position- or effort-clamped still skips its own velocity check, which is what your existing test_partial_clamp_preserves_other_actions asserts and what a new control test here pins down. The chunk-level previous_safe reset that #277 mentions as compounding is left alone — separate concern, separate PR, per your one-concern-per-PR rule.

How it was verified

pytest tests/ on 97d2a87 before the change: 3217 passed, 75 skipped, none failing. After the change: 3222 passed, 75 skipped, none failing. The five extra are new tests in tests/test_guard.py. Three of them fail on the unpatched tree and pass with it — test_velocity_clamp_survives_position_violation_on_earlier_joint and test_velocity_clamp_survives_effort_violation_on_earlier_joint cover check_single() directly, and test_chunk_velocity_clamp_survives_position_violation_on_earlier_joint covers the chunk path check() that serve calls. The other two are controls that stay green either way: same-joint precedence, and joints with no configured velocity limit. ruff check and mypy --config-file=pyproject.toml on src/tether/safety/guard.py report exactly the same pre-existing findings before and after.

How this was managed

We imported this repo's issues and pull requests into a live board — 287 stories and 38 labels — and worked this change as the story for #277 on it. The story is at https://eastagiletracker.com/projects/323/stories/209656 and the board is at https://eastagiletracker.com/projects/323.

board

If you'd rather not receive contributions like this, reply no-more-prs on this pull request and we won't open any further ones on your repositories.


Lawrence W. Sinclair
CEO / East Agile
linkedin.com/in/lwsinclair/
eastagile.com

…tion

The velocity gate in check_single() read the whole cross-joint violations
list, so once any joint recorded a position or effort violation the gate
was false for every remaining joint and their velocity deltas were never
checked or clamped. Scope the gate to the joint being checked.

Position/effort precedence on the same joint is unchanged; a joint that
was itself clamped still skips its own velocity check.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[R1] Velocity clamp silently disabled after any earlier joint violation **[safety]**

1 participant