11"""Test that ServoL joint-velocity clamping doesn't cause TCP lurching.
22
3- When IK requires joint velocities that exceed hardware limits (e.g. near a
4- wrist singularity), ServoLCommand clamps the joint deltas. This can cause
5- the CSE's internal Cartesian state to diverge from the actual robot TCP.
6-
7- The fix re-syncs the CSE position when exiting a clamp period so there's no
8- accumulated gap to snap across. This test verifies that the per-tick TCP
9- displacement stays within bounds throughout the entire motion — including
10- the clamp → free transition.
3+ When IK requires joint velocities that exceed hardware limits, ServoLCommand
4+ clamps the joint deltas proportionally. The per-tick CSE position correction
5+ keeps the Cartesian planner in sync with the actual robot joints, preventing
6+ gap accumulation and lurch on clamp exit.
7+
8+ This test verifies that:
9+ 1. Clamping actually occurs (the test is meaningful)
10+ 2. No single tick produces a TCP displacement larger than limits allow
11+ 3. The final position converges accurately despite clamping
1112"""
1213
1314import math
2223
2324
2425_q_rad_buf = np .zeros (6 , dtype = np .float64 )
25- _T_buf = np .asfortranarray (
26- np .eye (4 , dtype = np .float64 )
27- ) # F-order for pinokin Eigen binding
26+ _T_buf = np .asfortranarray (np .eye (4 , dtype = np .float64 ))
2827_rpy_buf = np .zeros (3 , dtype = np .float64 )
2928
3029
@@ -39,7 +38,7 @@ def _home_steps() -> np.ndarray:
3938
4039
4140def _fk_mm_rpy (q_steps : np .ndarray ) -> np .ndarray :
42- """FK from steps → [x_mm, y_mm, z_mm, rx_deg, ry_deg, rz_deg]."""
41+ """FK from steps -> [x_mm, y_mm, z_mm, rx_deg, ry_deg, rz_deg]."""
4342 from pinokin import so3_rpy
4443
4544 steps_to_rad (np .asarray (q_steps , dtype = np .int32 ), _q_rad_buf )
@@ -59,64 +58,55 @@ def _fk_mm_rpy(q_steps: np.ndarray) -> np.ndarray:
5958
6059
6160class TestServoLClampResync :
62- """Verify no TCP lurch when exiting joint-velocity clamping."""
61+ """Verify no TCP lurch when joint-velocity clamping is active ."""
6362
64- def test_no_tcp_lurch_through_wrist_rotation (self ) -> None :
65- """A large wrist rotation target should not cause per-tick TCP jumps .
63+ def test_no_lurch_and_accurate_convergence (self ) -> None :
64+ """A linear target that triggers clamping should converge accurately .
6665
67- The target rotates RZ by 90° from home, which forces large J4/J6
68- deltas that trigger clamping . We verify that:
66+ A +30mm X offset from home triggers clamping because the IK solution
67+ requires joint velocities exceeding hardware limits . We verify that:
6968 1. Clamping actually occurs (the test is meaningful)
70- 2. No single tick produces a TCP displacement larger than what the
71- CSE's max velocity allows (with generous margin for Ruckig jerk).
69+ 2. No single tick produces a TCP displacement larger than limits allow
70+ 3. The motion converges to <1mm of the target
7271 """
7372 state = ControllerState ()
7473 home = _home_steps ()
7574 state .Position_in [:] = home
7675
77- # Target: same position, rotated 90° around Z
7876 home_pose = _fk_mm_rpy (home )
7977 target_pose = list (home_pose )
80- target_pose [5 ] += 90 .0 # rotate RZ by 90° (crosses wrist singularity)
78+ target_pose [0 ] += 30 .0 # +30mm X triggers clamping
8179
8280 cmd = ServoLCommand (ServoLCmd (pose = target_pose , speed = 1.0 , accel = 1.0 ))
8381 cmd .do_setup (state )
8482
85- # Max displacement per tick from CSE limits (generous 3x margin for jerk overshoot)
86- max_lin_per_tick = LIMITS .cart .jog .velocity .linear * INTERVAL_S * 3.0 # meters
87- max_ang_per_tick = LIMITS .cart .jog .velocity .angular * INTERVAL_S * 3.0 # rad
83+ # Max displacement per tick (generous 3x margin for Ruckig jerk overshoot)
84+ max_lin_per_tick = LIMITS .cart .jog .velocity .linear * INTERVAL_S * 3.0
85+ max_ang_per_tick = LIMITS .cart .jog .velocity .angular * INTERVAL_S * 3.0
8886
8987 prev_pose_mm_rpy : np .ndarray | None = None
9088 clamped_any = False
91- max_lin_jump = 0.0
92- max_ang_jump = 0.0
9389
9490 for tick in range (2000 ):
9591 cmd .do_setup (state )
9692 status = cmd .execute_step (state )
9793
98- # Simulate firmware tracking: Position_in follows Position_out
9994 state .Position_in [:] = state .Position_out
10095
10196 current_pose = _fk_mm_rpy (state .Position_in )
10297
10398 if prev_pose_mm_rpy is not None :
10499 lin_delta = np .linalg .norm (current_pose [:3 ] - prev_pose_mm_rpy [:3 ])
105- # Angle deltas (handle wrapping)
106- ang_deltas = []
107- for i in range (3 ):
108- d = abs (
100+ ang_deltas = [
101+ abs (
109102 (current_pose [3 + i ] - prev_pose_mm_rpy [3 + i ] + 180 ) % 360
110103 - 180
111104 )
112- ang_deltas .append (d )
105+ for i in range (3 )
106+ ]
113107 ang_delta = max (ang_deltas )
114108
115- max_lin_jump = max (max_lin_jump , lin_delta )
116- max_ang_jump = max (max_ang_jump , ang_delta )
117-
118- # Check: no single tick exceeds CSE velocity limits
119- assert lin_delta < max_lin_per_tick * 1000.0 , ( # convert m to mm
109+ assert lin_delta < max_lin_per_tick * 1000.0 , (
120110 f"Tick { tick } : linear TCP jump { lin_delta :.3f} mm exceeds "
121111 f"limit { max_lin_per_tick * 1000 :.3f} mm"
122112 )
@@ -125,7 +115,6 @@ def test_no_tcp_lurch_through_wrist_rotation(self) -> None:
125115 f"limit { math .degrees (max_ang_per_tick ):.3f} °"
126116 )
127117
128- # Detect if clamping occurred (cmd._clamped is set in execute_step)
129118 if cmd ._clamped :
130119 clamped_any = True
131120
@@ -134,8 +123,14 @@ def test_no_tcp_lurch_through_wrist_rotation(self) -> None:
134123 if status .name == "COMPLETED" :
135124 break
136125
137- # Verify the test actually exercised the clamp path
138126 assert clamped_any , (
139127 "Test did not trigger joint-velocity clamping — "
140- "adjust target to force a wrist singularity crossing"
128+ "adjust target to force clamping"
129+ )
130+
131+ # Verify final accuracy
132+ final_pose = _fk_mm_rpy (state .Position_in )
133+ pos_error = np .linalg .norm (np .array (final_pose [:3 ]) - np .array (target_pose [:3 ]))
134+ assert pos_error < 1.0 , (
135+ f"Position error { pos_error :.3f} mm exceeds 1.0mm tolerance"
141136 )
0 commit comments