Skip to content

Commit 5f16a80

Browse files
committed
Add tendon activation tolerance
Delay inactive dynamic-roller activation by a configurable fraction of the roller radius while preserving zero-clearance deactivation. This prevents route-state chatter near the switching boundary.
1 parent 990a7a0 commit 5f16a80

4 files changed

Lines changed: 52 additions & 1 deletion

File tree

‎newton/_src/solvers/tendon_kernels.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ def update_tendon_link_active(
137137
tendon_link_orientation: wp.array[int],
138138
tendon_link_offset: wp.array[wp.vec3],
139139
tendon_link_axis: wp.array[wp.vec3],
140+
tendon_activation_tol: float,
140141
tendon_link_active: wp.array[bool],
141142
):
142143
"""Update dynamic rolling links from oriented distance to their bypass span."""
@@ -232,7 +233,10 @@ def update_tendon_link_active(
232233
distance = wp.dot(candidate_offset - closest_offset, span_normal)
233234
if tendon_link_orientation[link_idx] <= 0:
234235
distance = -distance
235-
if alpha > 0.0 and alpha < 1.0 and distance <= tendon_link_radius[link_idx]:
236+
activation_radius = tendon_link_radius[link_idx]
237+
if not tendon_link_active[link_idx]:
238+
activation_radius = activation_radius * (1.0 - tendon_activation_tol)
239+
if alpha > 0.0 and alpha < 1.0 and distance <= activation_radius:
236240
active = True
237241
tendon_link_active[link_idx] = active
238242

‎newton/_src/solvers/tendon_state.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,17 @@ def _init_tendon_state(self, model: Model, allocate_xpbd_lambdas: bool = True) -
105105
self.tendon_max_sweeps = 256
106106
if not hasattr(self, "tendon_settle_tol"):
107107
self.tendon_settle_tol = 1.0e-3
108+
if not hasattr(self, "tendon_activation_tol"):
109+
self.tendon_activation_tol = 2.0e-3
108110
if not 1 <= self.tendon_max_sweeps <= 256:
109111
raise ValueError(f"tendon_max_sweeps must be between 1 and 256, got {self.tendon_max_sweeps}")
110112
if self.tendon_settle_tol < 0.0:
111113
raise ValueError(f"tendon_settle_tol must be non-negative, got {self.tendon_settle_tol}")
114+
if not 0.0 <= self.tendon_activation_tol < 1.0:
115+
raise ValueError(
116+
f"tendon_activation_tol must be between 0 (inclusive) and 1 (exclusive), "
117+
f"got {self.tendon_activation_tol}"
118+
)
112119
if model.tendon_segment_count == 0:
113120
self.tendon_seg_rest_length = None
114121
self.tendon_seg_rest_length_step = None
@@ -248,6 +255,7 @@ def _update_tendon_link_active(self, model: Model, body_q: wp.array[wp.transform
248255
model.tendon_link_orientation,
249256
model.tendon_link_offset,
250257
model.tendon_link_axis,
258+
self.tendon_activation_tol,
251259
self.tendon_link_active,
252260
],
253261
device=model.device,

‎newton/_src/solvers/xpbd/solver_xpbd.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def __init__(
117117
enable_restitution: bool = False,
118118
tendon_max_sweeps: int = 256,
119119
tendon_settle_tol: float = 1.0e-3,
120+
tendon_activation_tol: float = 2.0e-3,
120121
):
121122
super().__init__(model=model)
122123
self.iterations = iterations
@@ -126,6 +127,9 @@ def __init__(
126127
# cables stop almost immediately; stiff/transient ones run up to the cap.
127128
self.tendon_max_sweeps = tendon_max_sweeps
128129
self.tendon_settle_tol = tendon_settle_tol
130+
# Inactive dynamic rollers activate only after entering this fraction of their radius.
131+
# Active rollers still deactivate at zero clearance.
132+
self.tendon_activation_tol = tendon_activation_tol
129133

130134
self.soft_body_relaxation = soft_body_relaxation
131135
self.soft_contact_relaxation = soft_contact_relaxation

‎newton/tests/test_tendon_capstan.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,6 +1345,35 @@ def test_dynamic_route_uses_oriented_signed_distance(test, device):
13451345
)
13461346

13471347

1348+
def test_dynamic_route_activation_tolerance_preserves_boundary_state(test, device):
1349+
"""Dynamic routing should not chatter inside the activation tolerance."""
1350+
with wp.ScopedDevice(device):
1351+
radius = 0.1
1352+
activation_tol = 1.0e-3
1353+
activation_gap = activation_tol * radius
1354+
for orientation in (-1, 1):
1355+
model, candidate, candidate_link = build_oriented_dynamic_route(orientation, device)
1356+
solver = newton.solvers.SolverXPBD(
1357+
model,
1358+
iterations=1,
1359+
tendon_activation_tol=activation_tol,
1360+
)
1361+
body_q = model.body_q.numpy()
1362+
1363+
transitions = (
1364+
(radius - 0.5 * activation_gap, False),
1365+
(radius - 1.5 * activation_gap, True),
1366+
(radius - 0.5 * activation_gap, True),
1367+
(radius + 0.5 * activation_gap, False),
1368+
)
1369+
for distance, expected_active in transitions:
1370+
body_q[candidate, :3] = (distance * orientation, 0.0, 0.0)
1371+
model.body_q.assign(body_q)
1372+
solver._update_tendon_link_active(model, model.body_q)
1373+
actual_active = bool(solver.tendon_link_active.numpy()[candidate_link])
1374+
test.assertEqual(actual_active, expected_active)
1375+
1376+
13481377
def test_dynamic_route_requires_projection_on_bypass_span(test, device):
13491378
"""A roller beyond either neighboring site should remain inactive."""
13501379
with wp.ScopedDevice(device):
@@ -2566,6 +2595,12 @@ def run(settle_tol, max_sweeps):
25662595
devices,
25672596
test_dynamic_route_uses_oriented_signed_distance,
25682597
)
2598+
add_test(
2599+
TestTendonCapstan,
2600+
"dynamic_route_activation_tolerance_preserves_boundary_state",
2601+
devices,
2602+
test_dynamic_route_activation_tolerance_preserves_boundary_state,
2603+
)
25692604
add_test(
25702605
TestTendonCapstan,
25712606
"dynamic_route_requires_projection_on_bypass_span",

0 commit comments

Comments
 (0)