Skip to content

Commit ee6f52f

Browse files
Remove unnecessary gimbal test computation
1 parent c08b27a commit ee6f52f

1 file changed

Lines changed: 170 additions & 172 deletions

File tree

newton/tests/kamino/test_kamino_gimbal.py

Lines changed: 170 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,155 @@ def _build_rotational_d6(
7777
return builder.finalize(device="cpu"), d6
7878

7979

80+
@dataclass(frozen=True)
81+
class _Fixture:
82+
"""Model and D6 layout metadata for one conformance fixture."""
83+
84+
model: newton.Model
85+
q_start: int
86+
qd_start: int
87+
target_q_start: int
88+
89+
90+
@dataclass(frozen=True)
91+
class _Probe:
92+
"""Raw coordinate, velocity, and effort data from a solver rollout."""
93+
94+
q: np.ndarray
95+
qd: np.ndarray
96+
effort: np.ndarray
97+
coord_count: int
98+
dof_count: int
99+
100+
101+
def _build_fixture(
102+
fixed_base: bool,
103+
axes: tuple[newton.Axis, newton.Axis, newton.Axis],
104+
*,
105+
stiffness: float = 0.0,
106+
drive_damping: float = 0.0,
107+
armature: float = 0.0,
108+
passive_damping: float = 0.0,
109+
lower: float | np.ndarray = -newton.MAXVAL,
110+
upper: float | np.ndarray = newton.MAXVAL,
111+
) -> _Fixture:
112+
"""Build a collision-free articulated rotational D6."""
113+
builder = newton.ModelBuilder(gravity=(0.0, 0.0, 0.0), up_axis=newton.Axis.Z)
114+
base = builder.add_link(mass=2.0, inertia=_BASE_INERTIA, label="base")
115+
link = builder.add_link(mass=1.0, inertia=_LINK_INERTIA, label="link")
116+
root = (
117+
builder.add_joint_fixed(parent=-1, child=base, label="root")
118+
if fixed_base
119+
else builder.add_joint_free(parent=-1, child=base, label="root")
120+
)
121+
lower_values = np.broadcast_to(lower, 3)
122+
upper_values = np.broadcast_to(upper, 3)
123+
configs = [
124+
newton.ModelBuilder.JointDofConfig(
125+
axis=axis,
126+
target_pos=0.0,
127+
target_vel=0.0,
128+
target_ke=stiffness,
129+
target_kd=drive_damping,
130+
damping=passive_damping,
131+
armature=armature,
132+
limit_lower=float(lower_values[i]),
133+
limit_upper=float(upper_values[i]),
134+
limit_ke=1.0e4,
135+
limit_kd=100.0,
136+
)
137+
for i, axis in enumerate(axes)
138+
]
139+
d6 = builder.add_joint_d6(base, link, angular_axes=configs, label="d6")
140+
builder.add_articulation([root, d6], label="d6")
141+
model = builder.finalize(device=_DEVICE)
142+
return _Fixture(
143+
model,
144+
int(model.joint_q_start.numpy()[d6]),
145+
int(model.joint_qd_start.numpy()[d6]),
146+
int(model.joint_target_q_start.numpy()[d6]),
147+
)
148+
149+
150+
def _make_solver(backend: str, model: newton.Model) -> SolverKamino | SolverMuJoCo:
151+
"""Create a configured collision-free conformance solver."""
152+
if backend == "kamino":
153+
config = SolverKamino.Config(
154+
integrator="euler",
155+
use_collision_detector=False,
156+
use_fk_solver=False,
157+
sparse_jacobian=True,
158+
)
159+
config.constraints.alpha = 0.0
160+
config.constraints.beta = 0.1
161+
config.padmm.max_iterations = 200
162+
config.padmm.primal_tolerance = 1.0e-6
163+
config.padmm.dual_tolerance = 1.0e-6
164+
config.padmm.compl_tolerance = 1.0e-6
165+
return SolverKamino(model, config)
166+
if backend == "mjwarp":
167+
return SolverMuJoCo(
168+
model, disable_contacts=True, integrator="implicitfast", iterations=100, use_mujoco_contacts=False
169+
)
170+
raise ValueError(f"Unsupported conformance backend: {backend}")
171+
172+
173+
def _run(
174+
backend: str,
175+
fixed_base: bool,
176+
axes: tuple[newton.Axis, newton.Axis, newton.Axis],
177+
*,
178+
q: np.ndarray | None = None,
179+
qd: np.ndarray | None = None,
180+
effort: np.ndarray | None = None,
181+
position_target: np.ndarray | None = None,
182+
velocity_target: np.ndarray | None = None,
183+
steps: int = 1,
184+
**fixture_kwargs,
185+
) -> _Probe:
186+
"""Run a D6 rollout and retain the raw D6 coordinate trajectory."""
187+
fixture = _build_fixture(fixed_base, axes, **fixture_kwargs)
188+
state_in, state_out, control = fixture.model.state(), fixture.model.state(), fixture.model.control()
189+
if q is not None:
190+
values = state_in.joint_q.numpy()
191+
values[fixture.q_start : fixture.q_start + 3] = q
192+
state_in.joint_q.assign(values)
193+
if qd is not None:
194+
values = state_in.joint_qd.numpy()
195+
values[fixture.qd_start : fixture.qd_start + 3] = qd
196+
state_in.joint_qd.assign(values)
197+
if effort is not None:
198+
values = np.zeros(fixture.model.joint_dof_count, dtype=np.float32)
199+
values[fixture.qd_start : fixture.qd_start + 3] = effort
200+
control.joint_f.assign(values)
201+
if position_target is not None:
202+
values = control.joint_target_q.numpy()
203+
values[fixture.target_q_start : fixture.target_q_start + 3] = position_target
204+
control.joint_target_q.assign(values)
205+
if velocity_target is not None:
206+
values = control.joint_target_qd.numpy()
207+
values[fixture.qd_start : fixture.qd_start + 3] = velocity_target
208+
control.joint_target_qd.assign(values)
209+
newton.eval_fk(fixture.model, state_in.joint_q, state_in.joint_qd, state_in)
210+
solver = _make_solver(backend, fixture.model)
211+
contacts = Contacts(rigid_contact_max=0, soft_contact_max=0, device=_DEVICE) if backend == "mjwarp" else None
212+
positions = [state_in.joint_q.numpy()[fixture.q_start : fixture.q_start + 3].copy()]
213+
velocities = [state_in.joint_qd.numpy()[fixture.qd_start : fixture.qd_start + 3].copy()]
214+
for _ in range(steps):
215+
state_in.clear_forces()
216+
solver.step(state_in, state_out, control, contacts, _DT)
217+
state_in, state_out = state_out, state_in
218+
positions.append(state_in.joint_q.numpy()[fixture.q_start : fixture.q_start + 3].copy())
219+
velocities.append(state_in.joint_qd.numpy()[fixture.qd_start : fixture.qd_start + 3].copy())
220+
return _Probe(
221+
np.stack(positions),
222+
np.stack(velocities),
223+
control.joint_f.numpy()[fixture.qd_start : fixture.qd_start + 3].copy(),
224+
fixture.model.joint_coord_count,
225+
fixture.model.joint_dof_count,
226+
)
227+
228+
80229
class TestGimbal(unittest.TestCase):
81230
"""Verify the rotational D6 representation."""
82231

@@ -272,154 +421,27 @@ def test_fk_reset_preserves_left_handed_coordinates_and_rates(self):
272421
state.joint_qd.numpy()[qd_start : qd_start + 3], qd[qd_start : qd_start + 3], atol=1.0e-5
273422
)
274423

275-
276-
@dataclass(frozen=True)
277-
class _Fixture:
278-
"""Model and D6 layout metadata for one conformance fixture."""
279-
280-
model: newton.Model
281-
q_start: int
282-
qd_start: int
283-
target_q_start: int
284-
285-
286-
@dataclass(frozen=True)
287-
class _Probe:
288-
"""Raw coordinate, velocity, and effort data from a solver rollout."""
289-
290-
q: np.ndarray
291-
qd: np.ndarray
292-
effort: np.ndarray
293-
coord_count: int
294-
dof_count: int
295-
296-
297-
def _build_fixture(
298-
fixed_base: bool,
299-
axes: tuple[newton.Axis, newton.Axis, newton.Axis],
300-
*,
301-
stiffness: float = 0.0,
302-
drive_damping: float = 0.0,
303-
armature: float = 0.0,
304-
passive_damping: float = 0.0,
305-
lower: float | np.ndarray = -newton.MAXVAL,
306-
upper: float | np.ndarray = newton.MAXVAL,
307-
) -> _Fixture:
308-
"""Build a collision-free articulated rotational D6."""
309-
builder = newton.ModelBuilder(gravity=(0.0, 0.0, 0.0), up_axis=newton.Axis.Z)
310-
base = builder.add_link(mass=2.0, inertia=_BASE_INERTIA, label="base")
311-
link = builder.add_link(mass=1.0, inertia=_LINK_INERTIA, label="link")
312-
root = (
313-
builder.add_joint_fixed(parent=-1, child=base, label="root")
314-
if fixed_base
315-
else builder.add_joint_free(parent=-1, child=base, label="root")
316-
)
317-
lower_values = np.broadcast_to(lower, 3)
318-
upper_values = np.broadcast_to(upper, 3)
319-
configs = [
320-
newton.ModelBuilder.JointDofConfig(
321-
axis=axis,
322-
target_pos=0.0,
323-
target_vel=0.0,
324-
target_ke=stiffness,
325-
target_kd=drive_damping,
326-
damping=passive_damping,
327-
armature=armature,
328-
limit_lower=float(lower_values[i]),
329-
limit_upper=float(upper_values[i]),
330-
limit_ke=1.0e4,
331-
limit_kd=100.0,
332-
)
333-
for i, axis in enumerate(axes)
334-
]
335-
d6 = builder.add_joint_d6(base, link, angular_axes=configs, label="d6")
336-
builder.add_articulation([root, d6], label="d6")
337-
model = builder.finalize(device=_DEVICE)
338-
return _Fixture(
339-
model,
340-
int(model.joint_q_start.numpy()[d6]),
341-
int(model.joint_qd_start.numpy()[d6]),
342-
int(model.joint_target_q_start.numpy()[d6]),
343-
)
344-
345-
346-
def _make_solver(backend: str, model: newton.Model) -> SolverKamino | SolverMuJoCo:
347-
"""Create a configured collision-free conformance solver."""
348-
if backend == "kamino":
349-
config = SolverKamino.Config(
350-
integrator="euler",
351-
use_collision_detector=False,
352-
use_fk_solver=False,
353-
sparse_jacobian=True,
354-
)
355-
config.constraints.alpha = 0.0
356-
config.constraints.beta = 0.1
357-
config.padmm.max_iterations = 200
358-
config.padmm.primal_tolerance = 1.0e-6
359-
config.padmm.dual_tolerance = 1.0e-6
360-
config.padmm.compl_tolerance = 1.0e-6
361-
return SolverKamino(model, config)
362-
if backend == "mjwarp":
363-
return SolverMuJoCo(
364-
model, disable_contacts=True, integrator="implicitfast", iterations=100, use_mujoco_contacts=False
365-
)
366-
raise ValueError(f"Unsupported conformance backend: {backend}")
367-
368-
369-
def _run(
370-
backend: str,
371-
fixed_base: bool,
372-
axes: tuple[newton.Axis, newton.Axis, newton.Axis],
373-
*,
374-
q: np.ndarray | None = None,
375-
qd: np.ndarray | None = None,
376-
effort: np.ndarray | None = None,
377-
position_target: np.ndarray | None = None,
378-
velocity_target: np.ndarray | None = None,
379-
steps: int = 1,
380-
**fixture_kwargs,
381-
) -> _Probe:
382-
"""Run a D6 rollout and retain the raw D6 coordinate trajectory."""
383-
fixture = _build_fixture(fixed_base, axes, **fixture_kwargs)
384-
state_in, state_out, control = fixture.model.state(), fixture.model.state(), fixture.model.control()
385-
if q is not None:
386-
values = state_in.joint_q.numpy()
387-
values[fixture.q_start : fixture.q_start + 3] = q
388-
state_in.joint_q.assign(values)
389-
if qd is not None:
390-
values = state_in.joint_qd.numpy()
391-
values[fixture.qd_start : fixture.qd_start + 3] = qd
392-
state_in.joint_qd.assign(values)
393-
if effort is not None:
394-
values = np.zeros(fixture.model.joint_dof_count, dtype=np.float32)
395-
values[fixture.qd_start : fixture.qd_start + 3] = effort
396-
control.joint_f.assign(values)
397-
if position_target is not None:
398-
values = control.joint_target_q.numpy()
399-
values[fixture.target_q_start : fixture.target_q_start + 3] = position_target
400-
control.joint_target_q.assign(values)
401-
if velocity_target is not None:
402-
values = control.joint_target_qd.numpy()
403-
values[fixture.qd_start : fixture.qd_start + 3] = velocity_target
404-
control.joint_target_qd.assign(values)
405-
newton.eval_fk(fixture.model, state_in.joint_q, state_in.joint_qd, state_in)
406-
solver = _make_solver(backend, fixture.model)
407-
contacts = Contacts(rigid_contact_max=0, soft_contact_max=0, device=_DEVICE) if backend == "mjwarp" else None
408-
positions = [state_in.joint_q.numpy()[fixture.q_start : fixture.q_start + 3].copy()]
409-
velocities = [state_in.joint_qd.numpy()[fixture.qd_start : fixture.qd_start + 3].copy()]
410-
for _ in range(steps):
411-
state_in.clear_forces()
412-
solver.step(state_in, state_out, control, contacts, _DT)
413-
state_in, state_out = state_out, state_in
414-
positions.append(state_in.joint_q.numpy()[fixture.q_start : fixture.q_start + 3].copy())
415-
velocities.append(state_in.joint_qd.numpy()[fixture.qd_start : fixture.qd_start + 3].copy())
416-
return _Probe(
417-
np.stack(positions),
418-
np.stack(velocities),
419-
control.joint_f.numpy()[fixture.qd_start : fixture.qd_start + 3].copy(),
420-
fixture.model.joint_coord_count,
421-
fixture.model.joint_dof_count,
422-
)
424+
def test_limits(self):
425+
"""Drive each D6 coordinate to its own position limit."""
426+
lower = np.array([-0.15, -0.25, -0.35], dtype=np.float32)
427+
upper = np.array([0.2, 0.3, 0.4], dtype=np.float32)
428+
target = np.array([0.6, -0.6, 0.6], dtype=np.float32)
429+
expected = np.where(target > 0.0, upper, lower)
430+
for axes in (_RH_AXES, _LH_AXES):
431+
for fixed_base in (True, False):
432+
with self.subTest(axes=axes, fixed_base=fixed_base):
433+
probe = _run(
434+
"kamino",
435+
fixed_base,
436+
axes,
437+
position_target=target,
438+
stiffness=100.0,
439+
drive_damping=15.0,
440+
lower=lower,
441+
upper=upper,
442+
steps=50,
443+
)
444+
np.testing.assert_allclose(probe.q[-1], expected, atol=1.0e-2, rtol=0.0)
423445

424446

425447
@unittest.skipUnless(wp.get_cuda_device_count(), "requires CUDA device")
@@ -530,30 +552,6 @@ def test_unwrapped_pd_targets(self):
530552
**kwargs,
531553
)
532554

533-
def test_limits(self):
534-
"""Drive each D6 coordinate to its own position limit."""
535-
lower = np.array([-0.15, -0.25, -0.35], dtype=np.float32)
536-
upper = np.array([0.2, 0.3, 0.4], dtype=np.float32)
537-
target = np.array([0.6, -0.6, 0.6], dtype=np.float32)
538-
expected = np.where(target > 0.0, upper, lower)
539-
for axes in (_RH_AXES, _LH_AXES):
540-
for fixed_base in (True, False):
541-
with self.subTest(axes=axes, fixed_base=fixed_base):
542-
for backend in ("mjwarp", "kamino"):
543-
with self.subTest(backend=backend):
544-
probe = _run(
545-
backend,
546-
fixed_base,
547-
axes,
548-
position_target=target,
549-
stiffness=100.0,
550-
drive_damping=15.0,
551-
lower=lower,
552-
upper=upper,
553-
steps=120,
554-
)
555-
np.testing.assert_allclose(probe.q[-1], expected, atol=1.0e-2, rtol=0.0)
556-
557555
def test_passive_damping(self):
558556
"""Match passive-damping D6 behavior."""
559557
for axes in (_RH_AXES, _LH_AXES):

0 commit comments

Comments
 (0)