Skip to content

Commit a16e12b

Browse files
Optimize test suite runtime (~18% faster) (#1689)
1 parent 36e8ada commit a16e12b

13 files changed

Lines changed: 1640 additions & 1550 deletions

newton/tests/test_collision_pipeline.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def test_collision_pipeline(
276276
shape_type_b=shape_type_b,
277277
broad_phase=broad_phase,
278278
)
279-
for _ in range(200):
279+
for _ in range(100):
280280
setup.step()
281281
setup.render()
282282
setup.test(test_level_a, 0, tolerance=tolerance)
@@ -386,7 +386,7 @@ def test_mesh_mesh_sdf_modes(
386386
sdf_max_resolution_a=sdf_max_resolution_a,
387387
sdf_max_resolution_b=sdf_max_resolution_b,
388388
)
389-
for _ in range(200):
389+
for _ in range(100):
390390
setup.step()
391391
setup.render()
392392
setup.test(TestLevel.VELOCITY_YZ, 0, tolerance=tolerance)
@@ -552,10 +552,12 @@ def _contact_pairs(broad_phase):
552552
contacts = pipeline.contacts()
553553
pipeline.collide(state, contacts)
554554
n = contacts.rigid_contact_count.numpy()[0]
555+
shape0_np = contacts.rigid_contact_shape0.numpy()
556+
shape1_np = contacts.rigid_contact_shape1.numpy()
555557
pairs = set()
556558
for i in range(n):
557-
s0 = int(contacts.rigid_contact_shape0.numpy()[i])
558-
s1 = int(contacts.rigid_contact_shape1.numpy()[i])
559+
s0 = int(shape0_np[i])
560+
s1 = int(shape1_np[i])
559561
pairs.add((min(s0, s1), max(s0, s1)))
560562
return pairs
561563

newton/tests/test_equality_constraints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def test_multiple_constraints(self):
7171
self.state_0, self.state_1 = self.model.state(), self.model.state()
7272
newton.eval_fk(self.model, self.model.joint_q, self.model.joint_qd, self.state_0)
7373

74-
for _ in range(1000):
74+
for _ in range(200):
7575
for _ in range(10):
7676
self.state_0.clear_forces()
7777
self.solver.step(self.state_0, self.state_1, self.control, None, self.sim_dt)

newton/tests/test_fixed_tendon.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import unittest
1717
from enum import IntEnum
1818

19+
import warp as wp
20+
1921
import newton
2022
from newton.solvers import SolverMuJoCo
2123

@@ -99,7 +101,25 @@ def test_single_mujoco_fixed_tendon_length_behaviour(self):
99101
joint_start_positions = [0.5, 0.0, 0.5, 0.0]
100102
state_in.joint_q.assign(joint_start_positions)
101103

102-
for _i in range(0, 200):
104+
device = model.device
105+
use_cuda_graph = device.is_cuda and wp.is_mempool_enabled(device)
106+
if use_cuda_graph:
107+
# warmup (2 steps for full ping-pong cycle)
108+
solver.step(state_in=state_in, state_out=state_out, contacts=contacts, control=control, dt=dt)
109+
solver.step(state_in=state_out, state_out=state_in, contacts=contacts, control=control, dt=dt)
110+
with wp.ScopedCapture(device) as capture:
111+
solver.step(state_in=state_in, state_out=state_out, contacts=contacts, control=control, dt=dt)
112+
solver.step(state_in=state_out, state_out=state_in, contacts=contacts, control=control, dt=dt)
113+
graph = capture.graph
114+
115+
remaining = 200 - (4 if use_cuda_graph else 0)
116+
for _i in range(remaining // 2 if use_cuda_graph else remaining):
117+
if use_cuda_graph:
118+
wp.capture_launch(graph)
119+
else:
120+
solver.step(state_in=state_in, state_out=state_out, contacts=contacts, control=control, dt=dt)
121+
state_in, state_out = state_out, state_in
122+
if use_cuda_graph and remaining % 2 == 1:
103123
solver.step(state_in=state_in, state_out=state_out, contacts=contacts, control=control, dt=dt)
104124
state_in, state_out = state_out, state_in
105125

newton/tests/test_heightfield.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,25 @@ def test_heightfield_collision(self):
270270
control = model.control()
271271
sim_dt = 1.0 / 240.0
272272

273-
for _ in range(500):
273+
device = model.device
274+
use_cuda_graph = device.is_cuda and wp.is_mempool_enabled(device)
275+
if use_cuda_graph:
276+
# warmup (2 steps for full ping-pong cycle)
277+
solver.step(state_in, state_out, control, None, sim_dt)
278+
solver.step(state_out, state_in, control, None, sim_dt)
279+
with wp.ScopedCapture(device) as capture:
280+
solver.step(state_in, state_out, control, None, sim_dt)
281+
solver.step(state_out, state_in, control, None, sim_dt)
282+
graph = capture.graph
283+
284+
remaining = 500 - (4 if use_cuda_graph else 0)
285+
for _ in range(remaining // 2 if use_cuda_graph else remaining):
286+
if use_cuda_graph:
287+
wp.capture_launch(graph)
288+
else:
289+
solver.step(state_in, state_out, control, None, sim_dt)
290+
state_in, state_out = state_out, state_in
291+
if use_cuda_graph and remaining % 2 == 1:
274292
solver.step(state_in, state_out, control, None, sim_dt)
275293
state_in, state_out = state_out, state_in
276294

newton/tests/test_hydroelastic.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -367,19 +367,19 @@ def test_mujoco_hydroelastic_penetration_depth(test, device):
367367
substeps = 10
368368
sim_time = 3.0
369369
num_frames = int(sim_time / sim_dt)
370+
total_steps = num_frames * substeps
370371

371-
for _ in range(num_frames):
372-
for _ in range(substeps):
373-
state_0.clear_forces()
374-
# Apply external force to upper boxes
375-
forces = np.zeros(model.body_count * 6, dtype=np.float32)
376-
for body_idx in upper_body_indices:
377-
forces[body_idx * 6 + 2] = -external_force
378-
state_0.body_f.assign(forces)
379-
380-
collision_pipeline.collide(state_0, contacts)
381-
solver.step(state_0, state_1, control, contacts, sim_dt / substeps)
382-
state_0, state_1 = state_1, state_0
372+
# Pre-compute forces as a Warp array
373+
forces_np = np.zeros(model.body_count * 6, dtype=np.float32)
374+
for body_idx in upper_body_indices:
375+
forces_np[body_idx * 6 + 2] = -external_force
376+
precomputed_forces = wp.array(forces_np.reshape(model.body_count, 6), dtype=wp.spatial_vector, device=device)
377+
378+
for _ in range(total_steps):
379+
wp.copy(state_0.body_f, precomputed_forces)
380+
collision_pipeline.collide(state_0, contacts)
381+
solver.step(state_0, state_1, control, contacts, sim_dt / substeps)
382+
state_0, state_1 = state_1, state_0
383383

384384
# Check that upper cubes are near their original positions
385385
body_q = state_0.body_q.numpy()

newton/tests/test_import_mjcf.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from newton.solvers import SolverMuJoCo
3030

3131

32-
class TestImportMjcf(unittest.TestCase):
32+
class TestImportMjcfBasic(unittest.TestCase):
3333
def test_humanoid_mjcf(self):
3434
builder = newton.ModelBuilder()
3535
builder.default_shape_cfg.ke = 123.0
@@ -610,6 +610,8 @@ def test_replace_3d_hinge_with_ball_joint(self):
610610
# note we need to swap quaternion order wxyz -> xyzw
611611
np.testing.assert_allclose(joint_x_p.q, [0, 0, 0.7071068, 0.7071068], atol=1e-6)
612612

613+
614+
class TestImportMjcfGeometry(unittest.TestCase):
613615
def test_cylinder_shapes_preserved(self):
614616
"""Test that cylinder geometries are properly imported as cylinders, not capsules."""
615617
# Create MJCF content with cylinder geometry
@@ -1829,6 +1831,8 @@ def test_single_mujoco_fixed_tendon_auto_springlength(self):
18291831
msg=f"Expected tendon_length0: {expected_tendon_length0}, Measured: {measured_tendon_length0}",
18301832
)
18311833

1834+
1835+
class TestImportMjcfSolverParams(unittest.TestCase):
18321836
def test_solimplimit_parsing(self):
18331837
"""Test that solimplimit attribute is parsed correctly from MJCF."""
18341838
mjcf = """<?xml version="1.0" ?>
@@ -2902,6 +2906,8 @@ def test_default_inheritance(self):
29022906
else:
29032907
self.fail("Model should have mujoco.condim attribute")
29042908

2909+
2910+
class TestImportMjcfActuatorsFrames(unittest.TestCase):
29052911
def test_actuatorfrcrange_parsing(self):
29062912
"""Test that actuatorfrcrange is parsed from MJCF joint attributes and applied to joint effort limits."""
29072913
mjcf_content = """<?xml version="1.0" encoding="utf-8"?>
@@ -3741,6 +3747,8 @@ def test_joint_anchor_with_rotated_child_body(self):
37413747
# In xyzw format: [0, 0, sin(45°), cos(45°)] = [0, 0, 0.7071, 0.7071]
37423748
np.testing.assert_allclose(joint_X_p[3:7], [0, 0, 0.7071068, 0.7071068], atol=1e-5)
37433749

3750+
3751+
class TestImportMjcfComposition(unittest.TestCase):
37443752
def test_floating_true_creates_free_joint(self):
37453753
"""Test that floating=True creates a free joint for the root body."""
37463754
mjcf_content = """<?xml version="1.0" encoding="utf-8"?>

0 commit comments

Comments
 (0)