Skip to content

Commit 60d4e7b

Browse files
hougantc-nvdaclaude
andcommitted
fix(retargeters): clear Se3Rel smoothing state when grip pose is invalid
Re-arming _first_frame alone drops only the rebaseline sample; the smoothed-delta EMA buffers still hold pre-loss motion at full strength (they never decay while the invalid branch returns early), so the second valid frame after recovery blends stale motion into the output. Zero both buffers in the invalid-grip branch, matching the reset handler's convention for re-arming the first-frame baseline. Addresses CodeRabbit review feedback on #743. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Hougant Chen <hougantc@nvidia.com>
1 parent 1124d99 commit 60d4e7b

2 files changed

Lines changed: 34 additions & 2 deletions

File tree

src/core/retargeting_engine_tests/python/test_se3_retargeter_pose_validity.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,31 @@ def test_recovery_rebaselines_without_jump(self, retargeter):
150150

151151
delta = np.from_dlpack(outputs["ee_delta"][0])
152152
npt.assert_array_almost_equal(delta, np.zeros(6))
153+
154+
def test_invalid_grip_clears_smoothing_state(self, retargeter):
155+
"""Pre-loss motion must not bleed into the output after recovery.
156+
157+
The smoothed-delta EMA buffers must be cleared when the grip pose
158+
goes invalid; otherwise the second valid frame after recovery blends
159+
against stale pre-loss motion.
160+
"""
161+
inputs, outputs = _build_io(retargeter)
162+
163+
# Baseline, then a large motion so the smoothing buffers are nonzero.
164+
_fill_controller(inputs[_DEVICE], grip_valid=True)
165+
retargeter.compute(inputs, outputs, _make_context())
166+
_fill_controller(inputs[_DEVICE], grip_valid=True, position=(0.2, 0.2, 0.2))
167+
retargeter.compute(inputs, outputs, _make_context())
168+
169+
_fill_controller(inputs[_DEVICE], grip_valid=False)
170+
retargeter.compute(inputs, outputs, _make_context())
171+
172+
# Recovery: first valid frame rebaselines, second is stationary and
173+
# must emit zero — any residual comes from stale smoothing state.
174+
_fill_controller(inputs[_DEVICE], grip_valid=True, position=(0.5, 0.5, 0.5))
175+
retargeter.compute(inputs, outputs, _make_context())
176+
_fill_controller(inputs[_DEVICE], grip_valid=True, position=(0.5, 0.5, 0.5))
177+
retargeter.compute(inputs, outputs, _make_context())
178+
179+
delta = np.from_dlpack(outputs["ee_delta"][0])
180+
npt.assert_array_almost_equal(delta, np.zeros(6))

src/retargeters/se3_retargeter.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,10 +393,14 @@ def _compute_fn(self, inputs: RetargeterIO, outputs: RetargeterIO, context) -> N
393393
else:
394394
# A connected controller can report an invalid grip pose (e.g. at
395395
# rest with pose tracking lost) whose zero-norm orientation
396-
# Rotation.from_quat rejects. Emit a zero delta and re-arm the
397-
# first-frame baseline so the next valid frame does not jump.
396+
# Rotation.from_quat rejects. Emit a zero delta and, as in the
397+
# reset handler above, re-arm the first-frame baseline and clear
398+
# the smoothing state so the next valid frame neither jumps nor
399+
# blends with pre-loss motion.
398400
if not bool(inp[ControllerInputIndex.GRIP_IS_VALID]):
399401
self._first_frame = True
402+
self._smoothed_delta_pos = np.zeros(3)
403+
self._smoothed_delta_rot = np.zeros(3)
400404
ee_delta[0] = np.zeros(6, dtype=np.float32)
401405
return
402406
grip_pos = np.from_dlpack(inp[ControllerInputIndex.GRIP_POSITION])

0 commit comments

Comments
 (0)