Skip to content

Commit 8f3ff1d

Browse files
committed
tests: add few quick check for forcing modules
1 parent e3acd1e commit 8f3ff1d

4 files changed

Lines changed: 332 additions & 0 deletions

File tree

tests/virtual_field/behavior/runtime/test_noel_c4_behavior.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ def test_server_accepts_noel_c4_mode() -> None:
199199
assert len(hello_ack["payload"]["arm_ids"]) == 2
200200

201201

202+
def test_backend_step_noel_c4_does_not_raise() -> None:
203+
backend = MultiArmPassThroughBackend()
204+
backend.register_user("user_noel_step", character_mode="noel-c4")
205+
scene_state = backend.step(1.0 / 120.0, command=None)
206+
assert len(scene_state.arms) == 2
207+
208+
202209
def test_server_accepts_cathy_foraging_mode() -> None:
203210
server = VRWebSocketServer(
204211
ssl_context=None, # type: ignore[arg-type]

tests/virtual_field/equations/runtime/test_cathy_throw_forces_equations.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
_pull_sphere_to_point_force,
1010
_PullSphereToPoint,
1111
_SphereBoxed,
12+
_TravelingContractingWave,
1213
)
1314

1415
pytestmark = pytest.mark.equations
@@ -207,6 +208,40 @@ def test_sucker_actuation_force_decays_with_distance() -> None:
207208
)
208209

209210

211+
def test_sucker_actuation_respects_sucker_index() -> None:
212+
rod = DummyRod()
213+
sphere = DummySphere(
214+
position=[0.03, 0.0, -0.05],
215+
velocity=[0.0, 0.0, 0.0],
216+
radius=0.01,
217+
mass=1.0,
218+
)
219+
kwargs = dict(k=10.0, nu=0.0, trigger=True, capture_distance=0.2)
220+
contact_all = SuckerActuationToSphere(**kwargs)
221+
contact_subset = SuckerActuationToSphere(**kwargs, sucker_index=[0])
222+
contact_none = SuckerActuationToSphere(**kwargs, sucker_index=[])
223+
224+
def clear_forces() -> None:
225+
rod.external_forces[:] = 0.0
226+
rod.external_torques[:] = 0.0
227+
sphere.external_forces[:] = 0.0
228+
sphere.external_torques[:] = 0.0
229+
230+
contact_all.apply_contact(rod, sphere)
231+
f_all = sphere.external_forces.copy()
232+
clear_forces()
233+
234+
contact_subset.apply_contact(rod, sphere)
235+
f_subset = sphere.external_forces.copy()
236+
clear_forces()
237+
238+
contact_none.apply_contact(rod, sphere)
239+
f_empty = sphere.external_forces.copy()
240+
241+
assert np.allclose(f_all, f_subset)
242+
assert np.allclose(f_empty, 0.0)
243+
244+
210245
def test_sucker_actuation_adds_alignment_torque() -> None:
211246
rod = DummyRod()
212247
sphere = DummySphere(
@@ -228,3 +263,51 @@ def test_sucker_actuation_adds_alignment_torque() -> None:
228263
assert np.linalg.norm(sphere.external_forces[:, 0]) > 0.0
229264
assert np.linalg.norm(sphere.external_torques[:, 0]) > 0.0
230265
assert np.linalg.norm(rod.external_torques[:, 0]) > 0.0
266+
267+
268+
def test_traveling_contracting_wave_also_modulates_stiffness() -> None:
269+
class DummyWaveRod:
270+
def __init__(self) -> None:
271+
self.total_elements = 6
272+
self.current_elements = 4
273+
self.rest_sigma = np.zeros((3, 6), dtype=np.float64)
274+
self.shear_matrix = np.zeros((3, 3, 6), dtype=np.float64)
275+
self.bend_matrix = np.zeros((3, 3, 6), dtype=np.float64)
276+
self.shear_matrix[0, 0, :] = 2.0
277+
self.shear_matrix[1, 1, :] = 3.0
278+
self.bend_matrix[0, 0, :] = 5.0
279+
self.bend_matrix[1, 1, :] = 7.0
280+
281+
rod = DummyWaveRod()
282+
event = {"count": 0}
283+
wave = _TravelingContractingWave(
284+
event_id=lambda: event["count"],
285+
original_rest_sigma=rod.rest_sigma,
286+
original_shear_matrix=rod.shear_matrix,
287+
original_bend_matrix=rod.bend_matrix,
288+
amplitude=-0.2,
289+
stiffness_amplitude=0.5,
290+
width=1.5,
291+
duration=0.4,
292+
)
293+
294+
wave.apply_forces(rod, time=0.0)
295+
assert np.allclose(rod.rest_sigma[2], 0.0)
296+
assert np.allclose(rod.shear_matrix[0, 0], 2.0)
297+
assert np.allclose(rod.bend_matrix[0, 0], 5.0)
298+
299+
event["count"] = 1
300+
wave.apply_forces(rod, time=0.1)
301+
302+
assert np.min(rod.rest_sigma[2, 2:]) < 0.0
303+
assert np.max(rod.shear_matrix[0, 0, 2:]) > 2.0
304+
assert np.max(rod.shear_matrix[1, 1, 2:]) > 3.0
305+
assert np.max(rod.bend_matrix[0, 0, 2:]) > 5.0
306+
assert np.max(rod.bend_matrix[1, 1, 2:]) > 7.0
307+
308+
wave.apply_forces(rod, time=1.0)
309+
assert np.allclose(rod.rest_sigma[2], 0.0)
310+
assert np.allclose(rod.shear_matrix[0, 0], 2.0)
311+
assert np.allclose(rod.shear_matrix[1, 1], 3.0)
312+
assert np.allclose(rod.bend_matrix[0, 0], 5.0)
313+
assert np.allclose(rod.bend_matrix[1, 1], 7.0)
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import numpy as np
2+
import pytest
3+
4+
from virtual_field.runtime.spirob_elastica.sdf_objects import (
5+
SDFObstacleCylinders,
6+
obstacle_cylinder_sdf_and_normal,
7+
)
8+
from virtual_field.runtime.spirob_elastica.sdf_objects_hash import (
9+
CylinderSpatialHash,
10+
SDFObstacleCylindersHash,
11+
obstacle_cylinder_sdf_and_normal_hash,
12+
)
13+
14+
pytestmark = pytest.mark.equations
15+
16+
17+
def _make_obstacles() -> tuple[np.ndarray, ...]:
18+
starts = np.array(
19+
[
20+
[0.0, 0.0, 0.0],
21+
[1.5, 0.0, 0.0],
22+
[0.0, 0.0, 1.5],
23+
],
24+
dtype=np.float64,
25+
)
26+
directions = np.array(
27+
[
28+
[0.0, 1.0, 0.0],
29+
[0.0, 1.0, 0.0],
30+
[1.0, 0.0, 0.0],
31+
],
32+
dtype=np.float64,
33+
)
34+
lengths = np.array([1.0, 1.0, 0.75], dtype=np.float64)
35+
radii = np.array([0.2, 0.2, 0.15], dtype=np.float64)
36+
normals = np.array(
37+
[
38+
[1.0, 0.0, 0.0],
39+
[1.0, 0.0, 0.0],
40+
[0.0, 1.0, 0.0],
41+
],
42+
dtype=np.float64,
43+
)
44+
return starts, directions, lengths, radii, normals
45+
46+
47+
def test_cylinder_spatial_hash_query_returns_local_candidates() -> None:
48+
starts, directions, lengths, radii, normals = _make_obstacles()
49+
spatial_hash = CylinderSpatialHash(
50+
starts=starts,
51+
directions=directions,
52+
lengths=lengths,
53+
radii=radii,
54+
normals=normals,
55+
cell_size=0.75,
56+
)
57+
58+
candidates = spatial_hash.query_candidates(np.array([0.1, 0.5, 0.0]))
59+
60+
assert 0 in candidates
61+
assert 1 not in candidates
62+
assert 2 not in candidates
63+
64+
65+
@pytest.mark.parametrize(
66+
"point",
67+
[
68+
np.array([0.1, 0.5, 0.0], dtype=np.float64),
69+
np.array([1.65, 0.25, 0.0], dtype=np.float64),
70+
np.array([0.4, 0.1, 1.55], dtype=np.float64),
71+
np.array([0.9, 0.9, 0.9], dtype=np.float64),
72+
],
73+
)
74+
def test_hash_sdf_matches_bruteforce_query(point: np.ndarray) -> None:
75+
starts, directions, lengths, radii, normals = _make_obstacles()
76+
spatial_hash = CylinderSpatialHash(
77+
starts=starts,
78+
directions=directions,
79+
lengths=lengths,
80+
radii=radii,
81+
normals=normals,
82+
cell_size=0.75,
83+
)
84+
85+
sdf_ref, normal_ref, idx_ref = obstacle_cylinder_sdf_and_normal(
86+
point, starts, directions, lengths, radii, normals
87+
)
88+
sdf_hash, normal_hash, idx_hash = obstacle_cylinder_sdf_and_normal_hash(
89+
point,
90+
starts,
91+
directions,
92+
lengths,
93+
radii,
94+
normals,
95+
spatial_hash,
96+
)
97+
98+
assert idx_hash == idx_ref
99+
assert np.isclose(sdf_hash, sdf_ref)
100+
assert np.allclose(normal_hash, normal_ref)
101+
102+
103+
def test_hash_force_matches_bruteforce_force_and_tip_penetration() -> None:
104+
starts, directions, lengths, radii, normals = _make_obstacles()
105+
positions = np.array(
106+
[
107+
[0.15, 0.15, 1.65],
108+
[0.25, 0.50, 0.50],
109+
[0.00, 0.00, 0.00],
110+
],
111+
dtype=np.float64,
112+
)
113+
velocities = np.array(
114+
[
115+
[0.0, -0.1, 0.0],
116+
[0.0, 0.0, 0.0],
117+
[0.0, 0.0, 0.0],
118+
],
119+
dtype=np.float64,
120+
)
121+
radius = np.array([0.05, 0.05, 0.05], dtype=np.float64)
122+
123+
class DummyRod:
124+
def __init__(self) -> None:
125+
self.position_collection = positions.copy()
126+
self.velocity_collection = velocities.copy()
127+
self.external_forces = np.zeros((3, positions.shape[1]), dtype=np.float64)
128+
self.radius = radius.copy()
129+
130+
tip_state_ref = {"arm": 0.0}
131+
tip_state_hash = {"arm": 0.0}
132+
133+
rod_ref = DummyRod()
134+
rod_hash = DummyRod()
135+
136+
brute_force = SDFObstacleCylinders(
137+
starts=starts,
138+
directions=directions,
139+
lengths=lengths,
140+
radii=radii,
141+
normals=normals,
142+
tip_penetration_state=tip_state_ref,
143+
tip_penetration_key="arm",
144+
stiffness=1000.0,
145+
damping=0.5,
146+
)
147+
hash_force = SDFObstacleCylindersHash(
148+
starts=starts,
149+
directions=directions,
150+
lengths=lengths,
151+
radii=radii,
152+
normals=normals,
153+
tip_penetration_state=tip_state_hash,
154+
tip_penetration_key="arm",
155+
stiffness=1000.0,
156+
damping=0.5,
157+
cell_size=0.75,
158+
)
159+
160+
brute_force.apply_forces(rod_ref)
161+
hash_force.apply_forces(rod_hash)
162+
163+
assert np.allclose(rod_hash.external_forces, rod_ref.external_forces)
164+
assert tip_state_hash["arm"] == pytest.approx(tip_state_ref["arm"])
165+
166+
167+
def test_hash_force_auto_expands_query_padding_for_larger_rod_radius() -> None:
168+
starts, directions, lengths, radii, normals = _make_obstacles()
169+
170+
class DummyRod:
171+
def __init__(self) -> None:
172+
self.position_collection = np.array([[0.15], [0.25], [0.0]])
173+
self.velocity_collection = np.zeros((3, 1), dtype=np.float64)
174+
self.external_forces = np.zeros((3, 1), dtype=np.float64)
175+
self.radius = np.array([0.3], dtype=np.float64)
176+
177+
rod = DummyRod()
178+
force = SDFObstacleCylindersHash(
179+
starts=starts,
180+
directions=directions,
181+
lengths=lengths,
182+
radii=radii,
183+
normals=normals,
184+
stiffness=1000.0,
185+
damping=0.0,
186+
)
187+
188+
assert force.query_padding == pytest.approx(np.max(radii))
189+
190+
force.apply_forces(rod)
191+
192+
assert force.query_padding == pytest.approx(0.3)
193+
assert rod.external_forces[0, 0] > 0.0

tests/virtual_field/modules/runtime/test_mode_base_modules.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33

44
from virtual_field.runtime.mode_base import OctoArmSimulationBase
5+
from virtual_field.runtime.two_gcr_simulation import TwoGCRSimulation
56

67
pytestmark = pytest.mark.modules
78

@@ -47,6 +48,41 @@ def handle_commands(self, command) -> None: # noqa: ANN001
4748
_ = command
4849

4950

51+
def _make_dummy_rod(base: list[float]) -> object:
52+
return type(
53+
"DummyRod",
54+
(),
55+
{
56+
"position_collection": np.array(
57+
[
58+
[base[0], base[0], base[0]],
59+
[base[1], base[1], base[1]],
60+
[base[2], base[2] - 0.2, base[2] - 0.4],
61+
],
62+
dtype=np.float64,
63+
),
64+
"radius": np.array([0.03, 0.02], dtype=np.float64),
65+
"lengths": np.array([0.2, 0.2], dtype=np.float64),
66+
"director_collection": np.repeat(
67+
np.eye(3, dtype=np.float64)[..., None], 2, axis=2
68+
),
69+
},
70+
)()
71+
72+
73+
class _TwoGCRInitRegression(TwoGCRSimulation):
74+
def build_simulation(self) -> None:
75+
self.wave_event_before_post_setup = self._grip_wave_event.get(
76+
self.arm_ids[0], 0
77+
)
78+
self.simulator = object()
79+
self.timestepper = _DummyStepper()
80+
self.rods = {
81+
arm_id: _make_dummy_rod(base)
82+
for arm_id, base in self.arm_bases.items()
83+
}
84+
85+
5086
def _base_position() -> tuple[float, float, float]:
5187
return (0.0, 1.0, -0.15)
5288

@@ -117,3 +153,16 @@ def test_arm_states_use_active_rod_slices_when_available() -> None:
117153
assert state.radii == [0.02]
118154
assert state.element_lengths == [0.2]
119155
assert len(state.directors) == 1
156+
157+
158+
def test_two_gcr_control_state_exists_during_build_simulation() -> None:
159+
simulation = _TwoGCRInitRegression(
160+
user_id="user_dummy",
161+
arm_ids=("left_arm", "right_arm"),
162+
base_left=[-0.15, 0.2, 0.15],
163+
base_right=[0.15, 0.2, 0.15],
164+
dt_internal=0.1,
165+
)
166+
167+
assert simulation.wave_event_before_post_setup == 0
168+
assert simulation._grip_wave_event == {"left_arm": 0, "right_arm": 0}

0 commit comments

Comments
 (0)