forked from isaac-sim/IsaacLab
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtest_articulation.py
More file actions
3440 lines (2927 loc) · 157 KB
/
Copy pathtest_articulation.py
File metadata and controls
3440 lines (2927 loc) · 157 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
# ignore private usage of variables warning
# pyright: reportPrivateUsage=none
"""Real-backend tests for the OVPhysX Articulation.
Mirrors :mod:`isaaclab_physx.test.assets.test_articulation` 1-to-1: same set
of test functions, names, parametrizations, and assertions.
OVPhysX runs kitless under ``./scripts/run_ovphysx.sh`` so there is no
``AppLauncher`` boot — :class:`~isaaclab.sim.SimulationContext` is driven
directly via ``build_simulation_context(sim_cfg=SimulationCfg(physics=OvPhysxCfg(), ...))``
which works because :func:`isaaclab.app.has_kit` returns False in this
environment.
PhysX-specific ``cube_object.root_view.set_X(...)`` / ``get_X(...)`` calls are
adapted to OVPhysX by going through
:attr:`~isaaclab_ov.assets.Articulation.root_view`, an
:class:`~isaaclab_ov.sim.views.OvPhysxView` over the per-tensor-type bindings
(``root_view.get_attribute(tensor_type)`` /
:meth:`~isaaclab_ov.assets.Articulation._get_binding`), and the public setters
(:meth:`set_masses_index`, :meth:`set_coms_index`, :meth:`set_inertias_index`).
Reads use the data-class properties (``cube_object.data.body_mass``,
``body_inertia``, ``body_com_pose_b``).
Process-global device lock
--------------------------
The OVPhysX runtime fixes device mode (CPU vs GPU) when the process creates
its first ``ovphysx.PhysX`` instance and cannot switch it without a process
restart. :class:`~isaaclab_ov.physics.OvPhysxManager` tracks
this on ``_locked_device`` and raises :exc:`RuntimeError` if a later
:class:`SimulationContext` requests a different device. The
``_ovphysx_skip_other_device`` autouse fixture below preempts that error in
parametrized tests by ``pytest.skip``-ing on the unlocked device, so the
session finishes cleanly when only one device is exercised.
CI note
-------
Because the lock is process-global, full coverage requires **two separate
``./scripts/run_ovphysx.sh -m pytest`` invocations** -- once with ``-k 'cpu'``
and once with ``-k 'cuda:0'``. Until the wheel exposes a way to reset Carbonite
device state, this is the supported pattern.
"""
from __future__ import annotations
import importlib
import sys
from pathlib import Path
from unittest.mock import Mock
import pytest
import torch
import warp as wp
from pxr import Usd, UsdGeom, UsdPhysics
from isaaclab.test.utils import test_devices
from isaaclab.test.utils.articulation_ordering import (
ANYMAL_C_PHYSX_JOINT_NAMES,
BRANCHING_MJWARP_BODY_NAMES,
BRANCHING_MJWARP_JOINT_NAMES,
BRANCHING_PHYSX_BODY_NAMES,
BRANCHING_PHYSX_JOINT_NAMES,
PANDA_ROOT_PRESERVING_REVERSED_BODY_NAMES,
)
# The OVPhysX runtime wheel is optional. Skip gracefully when it is not installed;
# CI jobs that need OVPhysX coverage install it explicitly.
pytest.importorskip("ovphysx.types", reason="ovphysx wheel not installed")
from isaaclab_ov import tensor_types as TT # noqa: E402
from isaaclab_ov.assets import Articulation # noqa: E402
from isaaclab_ov.assets.articulation.articulation_data import ArticulationData # noqa: E402
from isaaclab_ov.physics import OvPhysxCfg # noqa: E402
import isaaclab.sim as sim_utils # noqa: E402
import isaaclab.utils.math as math_utils # noqa: E402
import isaaclab.utils.string as string_utils # noqa: E402
from isaaclab.actuators import DelayedPDActuatorCfg, IdealPDActuatorCfg, ImplicitActuatorCfg # noqa: E402
from isaaclab.assets import ArticulationCfg, get_articulation_name_ordering # noqa: E402
from isaaclab.assets.articulation import ordering_kernels # noqa: E402
from isaaclab.envs.mdp.terminations import joint_effort_out_of_limit # noqa: E402
from isaaclab.managers import SceneEntityCfg # noqa: E402
from isaaclab.sim import SimulationCfg, build_simulation_context # noqa: E402
from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR # noqa: E402
from isaaclab.utils.version import get_isaac_sim_version, has_kit # noqa: E402
from isaaclab.utils.warp.launch_cache import _WarpLaunchCache # noqa: E402
##
# Pre-defined configs
##
from isaaclab_assets import ANYMAL_C_CFG, CARTPOLE_CFG, FRANKA_PANDA_CFG, SHADOW_HAND_CFG # isort:skip
wp.init()
pytestmark = pytest.mark.device_split
_OMNI_PHYSX_SCHEMAS_GAP_REASON = (
"Schema-level fixed-joint creation in :mod:`isaaclab.sim.schemas` imports the Kit-only "
"``omni.physx.scripts.utils`` module, which is not shipped by the ovphysx wheel."
)
_SPATIAL_TENDON_OVSTAGE_GAP_REASON = (
"OVPhysX 0.5.9 segfaults while attaching OVStage scenes containing spatial tendon schemas."
)
def test_cached_read_launches_reset_on_ordering_and_invalidation():
"""Ordering installation and simulation invalidation should discard recorded reads."""
class MinimalData(ArticulationData):
def __dir__(self):
return []
class Buffer:
timestamp = 1.0
data = MinimalData.__new__(MinimalData)
read_launch_cache = Mock()
data._read_launch_cache = read_launch_cache
data._configure_ordering_buffers = lambda: None
data._make_jacobian_body_user_to_backend = lambda: object()
data.joint_ordering = None
data._body_com_jacobian_w = Buffer()
data._mass_matrix = Buffer()
data._gravity_compensation_forces = Buffer()
data._apply_ordering_maps_after_resolve()
read_launch_cache.clear.assert_called_once_with()
assert data._body_com_jacobian_w.timestamp == -1.0
assert data._mass_matrix.timestamp == -1.0
assert data._gravity_compensation_forces.timestamp == -1.0
data._is_primed = True
data._sim_timestamp = 1.0
data._invalidate_initialize_callback(None)
assert read_launch_cache.clear.call_count == 2
assert data._is_primed is False
assert data._sim_timestamp == 0.0
def test_generalized_dynamics_reorder_uses_public_joint_order():
"""OVPhysX dynamics reads should gather both matrix joint axes into public order."""
class Buffer:
def __init__(self):
self.data = wp.zeros((1, 2, 2), dtype=wp.float32, device="cpu")
self.timestamp = -1.0
data = ArticulationData.__new__(ArticulationData)
data.device = "cpu"
data._sim_timestamp = 1.0
data._read_launch_cache = _WarpLaunchCache("cpu")
data.joint_ordering = object()
data._jacobian_joint_user_to_backend = wp.array([1, 0], dtype=wp.int32, device="cpu")
data._joint_dof_signs = wp.ones(2, dtype=wp.int32, device="cpu")
data._has_reversed_joints = False
data._num_base_dofs = 0
backend_values = wp.array([[[1.0, 2.0], [3.0, 4.0]]], dtype=wp.float32, device="cpu")
backend_buffer = wp.zeros_like(backend_values)
buffer = Buffer()
def read_binding(tensor_type, dst):
dst.assign(backend_values)
data._binding_read = read_binding
data._refresh_generalized_dynamics_buffer(
buffer,
backend_buffer,
TT.MASS_MATRIX,
ordering_kernels.reorder_mass_matrix_backend_to_user,
)
torch.testing.assert_close(
wp.to_torch(buffer.data),
torch.tensor([[[4.0, 3.0], [2.0, 1.0]]]),
)
assert buffer.timestamp == 1.0
def _read_binding_to_torch(articulation: Articulation, tensor_type: int, device: str | torch.device) -> torch.Tensor:
"""Read an OVPhysX attribute into a torch tensor on *device*.
Test-side adapter for the verbatim PhysX mirror. PhysX cross-checks the
data class against the simulation via ``articulation.root_view.get_X()``
accessors; on OVPhysX we go through the equivalent
:meth:`~isaaclab_ov.sim.views.OvPhysxView.get_attribute`, which returns a
freshly allocated ``float32`` array on the attribute's native device (CPU for
CPU-only property types), then move the result to *device*.
"""
arr = articulation.root_view.get_attribute(tensor_type)
return wp.to_torch(arr).to(device)
# Session-locked device. Set on the first parametrized test that runs and
# never reassigned -- ovphysx's process-global device lock means subsequent
# tests on the other device must skip.
_LOCKED_DEVICE: list[str | None] = [None]
@pytest.fixture(autouse=True)
def _ovphysx_skip_other_device(request):
"""Skip tests whose ``device`` parameter mismatches the session-locked device.
The OVPhysX runtime locks process-global device mode when the process
creates its first ``ovphysx.PhysX`` instance, so any test parametrized to a
different device after the first ``sim.reset()`` would hit the manager's
:exc:`RuntimeError`. We detect the locked device on the
first encounter and skip subsequent tests on the other device with a clear
message so the run finishes cleanly rather than producing spurious failures.
"""
callspec = getattr(request.node, "callspec", None)
device = callspec.params.get("device") if callspec is not None else None
if device is None:
# Test does not parametrize on device (e.g. test_warmup_attach_stage_not_called_for_cpu).
return
locked = _LOCKED_DEVICE[0]
if locked is None:
_LOCKED_DEVICE[0] = device
return
if device != locked:
pytest.skip(
f"ovphysx process-global device lock is held by '{locked}'; cannot run '{device}' "
"tests in the same session. Run pytest twice (once per device) for full coverage."
)
def _ovphysx_sim_context(device: str, **kwargs):
"""Wrapper around :func:`build_simulation_context` that injects OVPhysX cfg.
PhysX tests pass ``device=device`` directly and let
:func:`build_simulation_context` build a default :class:`SimulationCfg`.
OVPhysX needs ``physics=OvPhysxCfg()`` set on the cfg so the manager
dispatches to OVPhysX rather than PhysX, so we build the cfg here and
pass it through. ``gravity_enabled`` is consumed locally (it is ignored
by ``build_simulation_context`` once a ``sim_cfg`` is provided).
``add_ground_plane``, ``auto_add_lighting``, and other kwargs continue
to flow through ``build_simulation_context`` as before.
"""
dt = kwargs.pop("dt", 1.0 / 60.0)
gravity_enabled = kwargs.pop("gravity_enabled", True)
use_newton_actuators = kwargs.pop("use_newton_actuators", False)
gravity = (0.0, 0.0, -9.81) if gravity_enabled else (0.0, 0.0, 0.0)
sim_cfg = SimulationCfg(
physics=OvPhysxCfg(),
device=device,
dt=dt,
gravity=gravity,
use_newton_actuators=use_newton_actuators,
)
return build_simulation_context(device=device, sim_cfg=sim_cfg, **kwargs)
def generate_articulation_cfg(
articulation_type: str,
stiffness: float | None = 10.0,
damping: float | None = 2.0,
actuator_velocity_limit: float | None = None,
actuator_effort_limit: float | None = None,
joint_velocity_limit: float | None = None,
joint_effort_limit: float | None = None,
) -> ArticulationCfg:
"""Generate an articulation configuration.
Args:
articulation_type: Type of articulation to generate.
It should be one of: "humanoid", "panda", "anymal", "shadow_hand", "single_joint_implicit",
"single_joint_explicit".
stiffness: Stiffness value for the articulation's actuators. Only currently used for "humanoid".
Defaults to 10.0.
damping: Damping value for the articulation's actuators. Only currently used for "humanoid".
Defaults to 2.0.
actuator_velocity_limit: Velocity limit for the actuators. Only currently used for "single_joint_implicit"
and "single_joint_explicit".
actuator_effort_limit: Effort limit for explicit actuators. Only currently used for
"single_joint_explicit".
joint_velocity_limit: Velocity limit for the actuators (set into the simulation).
Only currently used for "single_joint_implicit" and "single_joint_explicit".
joint_effort_limit: Effort limit for the actuators (set into the simulation).
Only currently used for "single_joint_implicit" and "single_joint_explicit".
Returns:
The articulation configuration for the requested articulation type.
"""
if articulation_type == "humanoid":
articulation_cfg = ArticulationCfg(
spawn=sim_utils.UsdFileCfg(
usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/IsaacSim/Humanoid/humanoid_instanceable.usd"
),
init_state=ArticulationCfg.InitialStateCfg(pos=(0.0, 0.0, 1.34)),
actuators={"body": ImplicitActuatorCfg(joint_names_expr=[".*"], stiffness=stiffness, damping=damping)},
)
elif articulation_type == "panda":
articulation_cfg = FRANKA_PANDA_CFG
elif articulation_type == "anymal":
articulation_cfg = ANYMAL_C_CFG
elif articulation_type == "shadow_hand":
articulation_cfg = SHADOW_HAND_CFG
elif articulation_type == "single_joint_implicit":
articulation_cfg = ArticulationCfg(
# we set 80.0 default for max force because default in USD is 10e10 which makes testing annoying.
spawn=sim_utils.UsdFileCfg(
usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/IsaacSim/SimpleArticulation/revolute_articulation.usd",
joint_drive_props=sim_utils.JointDrivePropertiesCfg(max_effort=80.0, max_velocity=5.0),
),
actuators={
"joint": ImplicitActuatorCfg(
joint_names_expr=[".*"],
joint_effort_limit=joint_effort_limit,
joint_velocity_limit=joint_velocity_limit,
actuator_velocity_limit=actuator_velocity_limit,
stiffness=2000.0,
damping=100.0,
),
},
init_state=ArticulationCfg.InitialStateCfg(
pos=(0.0, 0.0, 0.0),
joint_pos=({"RevoluteJoint": 1.5708}),
rot=(0.7071081, 0, 0, 0.7071055),
),
)
elif articulation_type == "single_joint_explicit":
# we set 80.0 default for max force because default in USD is 10e10 which makes testing annoying.
articulation_cfg = ArticulationCfg(
spawn=sim_utils.UsdFileCfg(
usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/IsaacSim/SimpleArticulation/revolute_articulation.usd",
joint_drive_props=sim_utils.JointDrivePropertiesCfg(max_effort=80.0, max_velocity=5.0),
),
actuators={
"joint": IdealPDActuatorCfg(
joint_names_expr=[".*"],
joint_effort_limit=joint_effort_limit,
joint_velocity_limit=joint_velocity_limit,
actuator_effort_limit=actuator_effort_limit,
actuator_velocity_limit=actuator_velocity_limit,
stiffness=0.0,
damping=10.0,
),
},
)
elif articulation_type == "spatial_tendon_test_asset":
# we set 80.0 default for max force because default in USD is 10e10 which makes testing annoying.
articulation_cfg = ArticulationCfg(
spawn=sim_utils.UsdFileCfg(
usd_path=f"{ISAAC_NUCLEUS_DIR}/IsaacLab/Tests/spatial_tendons.usd",
),
actuators={
"joint": ImplicitActuatorCfg(
joint_names_expr=[".*"],
stiffness=2000.0,
damping=100.0,
),
},
)
else:
raise ValueError(
f"Invalid articulation type: {articulation_type}, valid options are 'humanoid', 'panda', 'anymal',"
" 'shadow_hand', 'single_joint_implicit', 'single_joint_explicit' or 'spatial_tendon_test_asset'."
)
return articulation_cfg
def generate_articulation(
articulation_cfg: ArticulationCfg, num_articulations: int, device: str
) -> tuple[Articulation, torch.tensor]:
"""Generate an articulation from a configuration.
Handles the creation of the articulation, the environment prims and the articulation's environment
translations
Args:
articulation_cfg: Articulation configuration.
num_articulations: Number of articulations to generate.
device: Device to use for the tensors.
Returns:
The articulation and environment translations.
"""
# Generate translations of 2.5 m in x for each articulation
translations = torch.zeros(num_articulations, 3, device=device)
translations[:, 0] = torch.arange(num_articulations) * 2.5
# Create Top-level Xforms, one for each articulation
for i in range(num_articulations):
sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=translations[i][:3])
articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_[^/]*/Robot"))
return articulation, translations
@pytest.mark.parametrize("device", ["cuda:0"])
def test_newton_native_explicit_actuator_submits_ovphysx_effort(device):
"""Run a Newton-native explicit actuator through the current OVPhysX state and effort binding."""
stiffness, damping, actuator_effort_limit = 20.0, 1.0, 80.0
with _ovphysx_sim_context(device=device, gravity_enabled=False, use_newton_actuators=True) as sim:
sim._app_control_on_stop_handle = None
articulation_cfg = generate_articulation_cfg("single_joint_explicit").replace(
actuators={
"joint": IdealPDActuatorCfg(
joint_names_expr=[".*"],
stiffness=stiffness,
damping=damping,
actuator_effort_limit=actuator_effort_limit,
)
}
)
articulation, _ = generate_articulation(articulation_cfg, 1, device)
sim.reset()
initial_pos = articulation.data.joint_pos.torch.clone()
target = initial_pos + 0.5
articulation.actuators.target_command.set_position_index(value=target)
articulation.write_data_to_sim()
assert articulation._actuator_control.native_actuator_path_active
assert articulation.newton_actuator_adapter is not None
assert torch.any(articulation.actuators.computed_effort.torch != 0.0)
assert torch.any(articulation.actuators.applied_effort.torch != 0.0)
torch.testing.assert_close(
_read_binding_to_torch(articulation, TT.DOF_ACTUATION_FORCE, device),
articulation.actuators.applied_effort.torch,
)
sim.step()
articulation.update(sim.cfg.dt)
# Use raw OV bindings so the observation cannot refresh the public state shadow.
current_pos = _read_binding_to_torch(articulation, TT.DOF_POSITION, device)
current_vel = _read_binding_to_torch(articulation, TT.DOF_VELOCITY, device)
assert not torch.allclose(current_pos, initial_pos)
articulation.write_data_to_sim()
expected_effort = torch.clamp(
stiffness * (target - current_pos) - damping * current_vel,
-actuator_effort_limit,
actuator_effort_limit,
)
torch.testing.assert_close(articulation.actuators.applied_effort.torch, expected_effort)
@pytest.mark.parametrize(
"module_name",
[
"isaaclab_physx.assets.articulation.actuator_control",
"isaaclab_ov.assets.articulation.actuator_control",
],
)
def test_host_actuator_control_import_does_not_probe_optional_newton_runtime(monkeypatch, module_name):
"""Import host controls without probing an unrequested Newton optional dependency."""
original_find_spec = importlib.util.find_spec
def reject_newton_probe(name, *args, **kwargs):
if name.startswith("isaaclab_newton"):
raise AssertionError("host actuator-control import eagerly probed Newton")
return original_find_spec(name, *args, **kwargs)
monkeypatch.setattr(importlib.util, "find_spec", reject_newton_probe)
importlib.reload(importlib.import_module(module_name))
@pytest.mark.parametrize("device", ["cuda:0"])
def test_newton_native_ovphysx_effort_binding_excludes_implicit_pd(device):
"""Submit raw native effort so OVPhysX evaluates the implicit joint drive once."""
with _ovphysx_sim_context(device=device, gravity_enabled=False, use_newton_actuators=True) as sim:
sim._app_control_on_stop_handle = None
articulation_cfg = CARTPOLE_CFG.replace(
actuators={
"cart": ImplicitActuatorCfg(
joint_names_expr=["slider_to_cart"], joint_effort_limit=400.0, stiffness=20.0, damping=0.0
),
"pole": IdealPDActuatorCfg(
joint_names_expr=["cart_to_pole"],
stiffness=20.0,
damping=0.0,
actuator_effort_limit=400.0,
),
}
)
articulation, _ = generate_articulation(articulation_cfg, 1, device)
sim.reset()
articulation.actuators.target_command.set_position_index(
value=articulation.data.joint_pos.torch + torch.tensor([[0.25, 0.5]], device=device)
)
articulation.write_data_to_sim()
raw_effort = wp.to_torch(articulation._physx_actuator_wrapper.joint_f_2d)
applied_effort = articulation.actuators.applied_effort.torch
assert torch.any(applied_effort[:, 0] != raw_effort[:, 0])
torch.testing.assert_close(
_read_binding_to_torch(articulation, TT.DOF_ACTUATION_FORCE, device),
raw_effort,
)
@pytest.mark.parametrize("device", ["cuda:0"])
def test_newton_native_actuator_reset_and_gain_event_are_environment_selective(device):
"""Reset and randomize only the selected OVPhysX native-controller environment."""
from isaaclab.envs.mdp.events import randomize_actuator_gains # noqa: PLC0415
from isaaclab.managers import EventTermCfg, SceneEntityCfg # noqa: PLC0415
class Env:
def __init__(self, asset):
self.scene = self
self.num_envs = asset.num_instances
self.device = asset.device
self._asset = asset
def __getitem__(self, name):
assert name == "robot"
return self._asset
with _ovphysx_sim_context(device=device, use_newton_actuators=True) as sim:
sim._app_control_on_stop_handle = None
articulation_cfg = generate_articulation_cfg("single_joint_explicit").replace(
actuators={
"joint": DelayedPDActuatorCfg(
joint_names_expr=[".*"],
stiffness=20.0,
damping=1.0,
actuator_effort_limit=80.0,
min_delay=1,
max_delay=1,
)
}
)
articulation, _ = generate_articulation(articulation_cfg, 2, device)
sim.reset()
for _ in range(3):
articulation.write_data_to_sim()
sim.step()
articulation.update(sim.cfg.dt)
adapter = articulation.newton_actuator_adapter
stateful_pairs = [
state
for actuator, state in zip(adapter.actuators, adapter._states_a)
if state is not None and getattr(state, "delay_state", None) is not None
]
assert len(stateful_pairs) == 1
articulation.reset(env_ids=torch.tensor([0], device=device, dtype=torch.long))
assert stateful_pairs[0].delay_state.num_pushes.numpy().tolist() == [0, 1]
env = Env(articulation)
asset_cfg = SceneEntityCfg("robot")
event_params = {
"asset_cfg": asset_cfg,
"stiffness_distribution_params": (101.0, 101.0),
"damping_distribution_params": (3.0, 3.0),
"operation": "abs",
"distribution": "uniform",
}
event = randomize_actuator_gains(EventTermCfg(func=randomize_actuator_gains, params=event_params), env)
event(env, env_ids=torch.tensor([0], device=device), **event_params)
from isaaclab.actuators.newton import read_group_parameter
stiffness = read_group_parameter(articulation.actuators, "joint", "controller", "kp")
damping = read_group_parameter(articulation.actuators, "joint", "controller", "kd")
torch.testing.assert_close(stiffness, torch.tensor([[101.0], [20.0]], device=device))
torch.testing.assert_close(damping, torch.tensor([[3.0], [1.0]], device=device))
@pytest.fixture
def sim(request):
"""Create simulation context with the specified device."""
device = request.getfixturevalue("device")
if "gravity_enabled" in request.fixturenames:
gravity_enabled = request.getfixturevalue("gravity_enabled")
else:
gravity_enabled = True # default to gravity enabled
if "add_ground_plane" in request.fixturenames:
add_ground_plane = request.getfixturevalue("add_ground_plane")
else:
add_ground_plane = False # default to no ground plane
with _ovphysx_sim_context(
device=device, auto_add_lighting=True, gravity_enabled=gravity_enabled, add_ground_plane=add_ground_plane
) as sim:
sim._app_control_on_stop_handle = None
yield sim
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
@pytest.mark.parametrize("gravity_enabled", [False])
def test_write_joint_state_accepts_int64_selector(sim, device, gravity_enabled):
"""Write joint state with int64 selectors."""
articulation_cfg = generate_articulation_cfg(articulation_type="panda")
articulation, _ = generate_articulation(articulation_cfg, 2, device=device)
sim.reset()
assert articulation.num_joints >= 2
env_ids = torch.tensor([1, 0], dtype=torch.int64, device=device)
joint_ids = torch.tensor([articulation.num_joints - 1, 0], dtype=torch.int64, device=device)
position = torch.tensor([[0.21, 0.11], [0.22, 0.12]], device=device)
velocity = torch.tensor([[1.21, 1.11], [1.22, 1.12]], device=device)
expected_position = articulation.data.joint_pos.torch.clone()
expected_velocity = articulation.data.joint_vel.torch.clone()
articulation.write_joint_state_to_sim_index(
position=position, velocity=velocity, env_ids=env_ids, joint_ids=joint_ids
)
expected_position[env_ids[:, None], joint_ids[None, :]] = position
expected_velocity[env_ids[:, None], joint_ids[None, :]] = velocity
torch.testing.assert_close(articulation.data.joint_pos.torch, expected_position)
torch.testing.assert_close(articulation.data.joint_vel.torch, expected_velocity)
@pytest.mark.parametrize("device", ["cpu"])
@pytest.mark.parametrize("gravity_enabled", [False])
def test_reversed_joint_dynamics_use_public_joint_basis(sim, device, gravity_enabled):
"""Keep dynamics tensors consistent with public joint velocity."""
articulation = Articulation(
ArticulationCfg(
prim_path="/World/Robot",
spawn=sim_utils.UsdFileCfg(
usd_path=str(Path(__file__).parent / "data" / "articulation_ordering_branching.usda")
),
actuators={},
)
)
UsdPhysics.FixedJoint.Define(sim.stage, "/World/Robot/fixed_root").GetBody1Rel().SetTargets(["/World/Robot/base"])
joint = UsdPhysics.RevoluteJoint.Get(sim.stage, "/World/Robot/left_elbow")
body0, body1 = joint.GetBody0Rel().GetTargets(), joint.GetBody1Rel().GetTargets()
joint.GetBody0Rel().SetTargets(body1)
joint.GetBody1Rel().SetTargets(body0)
sim.reset()
velocity = torch.zeros((1, articulation.num_joints), device=device)
velocity[:, articulation.find_joints("left_shoulder")[0][0]] = 0.4
velocity[:, articulation.find_joints("left_elbow")[0][0]] = 0.7
articulation.write_joint_velocity_to_sim_index(velocity=velocity)
sim.step()
articulation.update(sim.cfg.dt)
joint_velocity = articulation.data.joint_vel.torch
predicted_velocity = torch.einsum("nbij,nj->nbi", articulation.data.body_com_jacobian_w.torch, joint_velocity)
torch.testing.assert_close(predicted_velocity, articulation.data.body_com_vel_w.torch[:, 1:], atol=1e-5, rtol=1e-5)
generalized_energy = 0.5 * torch.einsum(
"ni,nij,nj->n", joint_velocity, articulation.data.mass_matrix.torch, joint_velocity
)
body_velocity = articulation.data.body_com_vel_w.torch
body_inertia = articulation.data.body_inertia.torch.reshape(1, articulation.num_bodies, 3, 3)
body_energy = 0.5 * (
(articulation.data.body_mass.torch.unsqueeze(-1) * body_velocity[..., :3].square()).sum((-1, -2))
+ torch.einsum("nbi,nbij,nbj->n", body_velocity[..., 3:], body_inertia, body_velocity[..., 3:])
)
torch.testing.assert_close(generalized_energy, body_energy, atol=1e-5, rtol=1e-5)
def test_joint_dof_sign_resolution_traverses_instance_proxies():
"""Resolve reversed joints inside an instanceable articulation."""
source_stage = Usd.Stage.CreateInMemory()
UsdGeom.Xform.Define(source_stage, "/Robot")
UsdGeom.Xform.Define(source_stage, "/Robot/base")
UsdGeom.Xform.Define(source_stage, "/Robot/link")
joint = UsdPhysics.RevoluteJoint.Define(source_stage, "/Robot/joint")
joint.GetBody0Rel().SetTargets(["/Robot/link"])
joint.GetBody1Rel().SetTargets(["/Robot/base"])
stage = Usd.Stage.CreateInMemory()
instance = UsdGeom.Xform.Define(stage, "/World/Robot").GetPrim()
instance.GetReferences().AddReference(source_stage.GetRootLayer().identifier, "/Robot")
instance.SetInstanceable(True)
articulation = Mock(
cfg=Mock(prim_path="/World/Robot"),
_joint_names=["joint"],
_body_names=["base", "link"],
)
assert Articulation._resolve_joint_dof_signs(articulation, stage) == (-1,)
@pytest.mark.parametrize("num_articulations", [1])
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_live_anymal_c_manual_joint_ordering_preserves_unselected_backend_state(sim, num_articulations, device):
"""Test that a partial ordered write preserves every unselected backend joint."""
articulation_cfg = generate_articulation_cfg("anymal").replace(
joint_ordering=tuple(reversed(ANYMAL_C_PHYSX_JOINT_NAMES))
)
articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device)
sim.reset()
joint_ordering = articulation.joint_ordering
assert joint_ordering is not None
backend_seed = torch.arange(1, articulation.num_joints + 1, dtype=torch.float32, device=device).reshape(1, -1)
backend_seed *= 0.001
articulation.root_view.set_attribute(TT.DOF_POSITION, wp.from_torch(backend_seed))
backend_before = wp.to_torch(articulation.root_view.get_attribute(TT.DOF_POSITION)).clone()
torch.testing.assert_close(backend_before, backend_seed, rtol=0.0, atol=0.0)
backend_joint_id = joint_ordering.user_to_backend_indices[0]
selected_value = backend_before[0, backend_joint_id] + 0.001
articulation.write_joint_position_to_sim_index(
position=selected_value.reshape(1, 1),
env_ids=wp.array([0], dtype=wp.int32, device=device),
joint_ids=wp.array([0], dtype=wp.int32, device=device),
)
backend_after = wp.to_torch(articulation.root_view.get_attribute(TT.DOF_POSITION)).clone()
expected = backend_before.clone()
expected[0, backend_joint_id] = selected_value
torch.testing.assert_close(backend_after, expected, rtol=0.0, atol=0.0)
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_live_anymal_c_manual_joint_ordering_reorders_joint_targets(sim, device):
"""Write nonidentity-ordered joint targets into their intended backend columns."""
backend_joint_names = ANYMAL_C_PHYSX_JOINT_NAMES
joint_ordering = (*backend_joint_names[1:], backend_joint_names[0])
articulation_cfg = generate_articulation_cfg("anymal").replace(
joint_ordering=joint_ordering,
actuators={"legs": ImplicitActuatorCfg(joint_names_expr=[".*"], stiffness=10.0, damping=2.0)},
)
articulation, _ = generate_articulation(articulation_cfg, 1, device=device)
sim.reset()
ordering = articulation.joint_ordering
assert ordering is not None
user_to_backend = torch.as_tensor(ordering.user_to_backend_indices, dtype=torch.long, device=device)
backend_to_user = torch.as_tensor(ordering.backend_to_user_indices, dtype=torch.long, device=device)
assert not torch.equal(user_to_backend, backend_to_user)
joint_index = torch.arange(articulation.num_joints, dtype=torch.float32, device=device).unsqueeze(0)
position_target = -0.25 + 0.031 * joint_index
velocity_target = 0.07 + 0.017 * joint_index
articulation.set_joint_position_target_index(target=position_target)
articulation.set_joint_velocity_target_index(target=velocity_target)
articulation.write_data_to_sim()
backend_position_target = _read_binding_to_torch(articulation, TT.DOF_POSITION_TARGET, device)
backend_velocity_target = _read_binding_to_torch(articulation, TT.DOF_VELOCITY_TARGET, device)
torch.testing.assert_close(backend_position_target, position_target[:, backend_to_user])
torch.testing.assert_close(backend_velocity_target, velocity_target[:, backend_to_user])
@pytest.mark.parametrize("device", ["cpu"])
def test_live_anymal_c_manual_joint_ordering_reorders_joint_friction_properties(sim, device):
"""Read every friction component from backend order into public joint order."""
backend_joint_names = ANYMAL_C_PHYSX_JOINT_NAMES
joint_ordering = (*backend_joint_names[1:], backend_joint_names[0])
articulation_cfg = generate_articulation_cfg("anymal").replace(joint_ordering=joint_ordering)
articulation, _ = generate_articulation(articulation_cfg, 1, device=device)
sim.reset()
ordering = articulation.joint_ordering
assert ordering is not None
user_to_backend = torch.as_tensor(ordering.user_to_backend_indices, dtype=torch.long, device=device)
joint_index = torch.arange(articulation.num_joints, dtype=torch.float32, device=device).unsqueeze(0)
backend_friction = torch.stack(
(20.0 + joint_index, 10.0 + 0.5 * joint_index, 1.0 + 0.25 * joint_index),
dim=-1,
)
articulation.root_view.set_attribute(
TT.DOF_FRICTION_PROPERTIES,
wp.from_torch(backend_friction.contiguous()),
)
articulation.data._joint_friction_props_buf.timestamp = -1.0
articulation.data._joint_friction_props_backend.timestamp = -1.0
expected = backend_friction[:, user_to_backend]
torch.testing.assert_close(articulation.data.joint_friction_coeff.torch, expected[..., 0])
torch.testing.assert_close(articulation.data.joint_dynamic_friction_coeff.torch, expected[..., 1])
torch.testing.assert_close(articulation.data.joint_viscous_friction_coeff.torch, expected[..., 2])
@pytest.mark.parametrize("selection", ["full", "partial"])
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_reversed_joint_ordering_joint_state_index_writes_backend_order(sim, selection, device):
"""Write full and partial indexed joint state through a nonidentity public joint axis."""
articulation_cfg = generate_articulation_cfg("anymal").replace(
joint_ordering=tuple(reversed(ANYMAL_C_PHYSX_JOINT_NAMES))
)
articulation, _ = generate_articulation(articulation_cfg, 2, device=device)
sim.reset()
ordering = articulation.joint_ordering
assert ordering is not None
num_joints = articulation.num_joints
user_to_backend = torch.as_tensor(ordering.user_to_backend_indices, dtype=torch.long, device=device)
backend_to_user = torch.as_tensor(ordering.backend_to_user_indices, dtype=torch.long, device=device)
backend_pos_before = torch.arange(2 * num_joints, dtype=torch.float32, device=device).reshape(2, num_joints)
backend_vel_before = backend_pos_before + 100.0
articulation.root_view.set_attribute(TT.DOF_POSITION, wp.from_torch(backend_pos_before.contiguous()))
articulation.root_view.set_attribute(TT.DOF_VELOCITY, wp.from_torch(backend_vel_before.contiguous()))
for buffer in (
articulation.data._joint_pos_buf,
articulation.data._joint_vel_buf,
articulation.data._joint_pos_backend,
articulation.data._joint_vel_backend,
):
if buffer is not None:
buffer.timestamp = -1.0
public_pos_before = articulation.data.joint_pos.torch.clone()
public_vel_before = articulation.data.joint_vel.torch.clone()
torch.testing.assert_close(public_pos_before, backend_pos_before[:, user_to_backend])
torch.testing.assert_close(public_vel_before, backend_vel_before[:, user_to_backend])
if selection == "full":
position = torch.arange(2 * num_joints, dtype=torch.float32, device=device).reshape(2, num_joints) + 200.0
velocity = position + 100.0
env_ids = None
joint_ids = None
expected_public_pos = position
expected_public_vel = velocity
expected_backend_pos = position[:, backend_to_user]
expected_backend_vel = velocity[:, backend_to_user]
else:
position = torch.tensor([[201.0, 203.0]], device=device)
velocity = torch.tensor([[301.0, 303.0]], device=device)
env_ids = [1]
joint_ids = [0, 2]
expected_public_pos = public_pos_before.clone()
expected_public_vel = public_vel_before.clone()
expected_public_pos[1, joint_ids] = position[0]
expected_public_vel[1, joint_ids] = velocity[0]
expected_backend_pos = backend_pos_before.clone()
expected_backend_vel = backend_vel_before.clone()
backend_joint_ids = user_to_backend[joint_ids]
expected_backend_pos[1, backend_joint_ids] = position[0]
expected_backend_vel[1, backend_joint_ids] = velocity[0]
articulation.write_joint_state_to_sim_index(
position=position,
velocity=velocity,
env_ids=env_ids,
joint_ids=joint_ids,
)
torch.testing.assert_close(articulation.data.joint_pos.torch, expected_public_pos)
torch.testing.assert_close(articulation.data.joint_vel.torch, expected_public_vel)
torch.testing.assert_close(
_read_binding_to_torch(articulation, TT.DOF_POSITION, device),
expected_backend_pos,
)
torch.testing.assert_close(
_read_binding_to_torch(articulation, TT.DOF_VELOCITY, device),
expected_backend_vel,
)
@pytest.mark.parametrize("num_articulations", [1])
# COM pose is a CPU-resident OVPhysX binding (``_CPU_ONLY_TYPES``) even on a GPU sim, and this test
# restores it via the low-level ``root_view.set_attribute`` which forbids cross-device staging, so it
# is inherently CPU-only (aligning it to ``cuda:0`` would fail on the CPU-native COM binding).
@pytest.mark.parametrize("device", ["cpu"])
def test_live_panda_manual_body_ordering_preserves_unselected_coms(sim, num_articulations, device):
"""Test that a partial ordered COM write preserves every unselected backend body."""
articulation_cfg = FRANKA_PANDA_CFG.replace(body_ordering=PANDA_ROOT_PRESERVING_REVERSED_BODY_NAMES)
articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device)
sim.reset()
body_ordering = articulation.body_ordering
assert body_ordering is not None
backend_before = _read_binding_to_torch(articulation, TT.BODY_COM_POSE, device).clone()
assert torch.unique(backend_before[0], dim=0).shape[0] > 1
articulation.data._body_com_pose_b.timestamp = -1.0
backend_staging = articulation.data._body_com_pose_b_backend
if backend_staging is not None:
backend_staging.timestamp = -1.0
public_body_id = 1
backend_body_id = body_ordering.user_to_backend_indices[public_body_id]
assert backend_body_id != public_body_id
selected_com = backend_before[0, backend_body_id].clone()
selected_com[0] += 0.001
articulation.set_coms_index(
coms=wp.from_torch(selected_com.reshape(1, 1, 7).contiguous(), dtype=wp.transformf),
env_ids=wp.array([0], dtype=wp.int32, device=device),
body_ids=wp.array([public_body_id], dtype=wp.int32, device=device),
)
backend_after = _read_binding_to_torch(articulation, TT.BODY_COM_POSE, device).clone()
articulation.root_view.set_attribute(TT.BODY_COM_POSE, wp.from_torch(backend_before.contiguous()))
noop_after = _read_binding_to_torch(articulation, TT.BODY_COM_POSE, device).clone()
unselected_body_mask = torch.ones(backend_before.shape[1], dtype=torch.bool, device=device)
unselected_body_mask[backend_body_id] = False
assert torch.equal(noop_after[..., :3], backend_before[..., :3])
assert torch.equal(backend_after[0, backend_body_id, :3], selected_com[:3])
assert torch.equal(backend_after[0, unselected_body_mask, :3], backend_before[0, unselected_body_mask, :3])
# Bound semantic orientation equality by the native setter's float32 no-op normalization.
native_orientation_atol = torch.max(
torch.abs(noop_after[0, unselected_body_mask, 3:7] - backend_before[0, unselected_body_mask, 3:7])
).item()
assert native_orientation_atol <= torch.finfo(backend_before.dtype).eps
torch.testing.assert_close(
backend_after[0, unselected_body_mask, 3:7],
backend_before[0, unselected_body_mask, 3:7],
rtol=0.0,
atol=native_orientation_atol,
)
assert torch.equal(backend_after[..., 3:7], noop_after[..., 3:7])
@pytest.mark.parametrize("num_articulations", [1])
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_reversed_body_ordering_wrench_composes_from_backend_pose_without_shadow_refresh(
sim, num_articulations, device
):
"""Reversed body ordering: an external wrench composes from the backend-order link pose and
``write_data_to_sim`` no longer refreshes the public ``body_link_pose_w`` shadow.
"""
articulation_cfg = FRANKA_PANDA_CFG.replace(body_ordering=PANDA_ROOT_PRESERVING_REVERSED_BODY_NAMES)
articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device)
sim.reset()
body_ordering = articulation.body_ordering
assert body_ordering is not None
# Apply a body-frame wrench to a single named body (public order).
body_ids, _ = articulation.find_bodies("panda_hand")
public_body_id = body_ids[0]
backend_body_id = int(body_ordering.user_to_backend_indices[public_body_id])
assert backend_body_id != public_body_id # exercises the reorder
force_b = torch.zeros(articulation.num_instances, len(body_ids), 3, device=device)
torque_b = torch.zeros(articulation.num_instances, len(body_ids), 3, device=device)
force_b[..., 0], force_b[..., 1], force_b[..., 2] = 3.0, -5.0, 7.0
torque_b[..., 0], torque_b[..., 1], torque_b[..., 2] = 0.5, -1.5, 2.5
articulation.permanent_wrench_composer.set_forces_and_torques_index(
forces=force_b, torques=torque_b, body_ids=body_ids
)
# Step once so the link poses are non-trivial (rotated), giving the quaternion rotation teeth.
articulation.set_joint_position_target_index(target=articulation.data.default_joint_pos.torch.clone())
articulation.write_data_to_sim()
sim.step()
articulation.update(sim.cfg.dt)
# write_data_to_sim must NOT advance the public body_link_pose_w shadow timestamp: the wrench
# path now reads the backend-order pose buffer instead of refreshing the public shadow.
shadow_ts_before = articulation.data._body_link_pose_w.timestamp
articulation.write_data_to_sim()
assert articulation.data._body_link_pose_w.timestamp == shadow_ts_before
# The wrench buffer is in backend order; the world-frame wrench must match the one composed
# from the SAME physical body's pose read via the public (user-order) shadow.
wrench_buf = wp.to_torch(articulation._wrench_buf).to(device)
pose = articulation.data.body_link_pose_w.torch[0, public_body_id] # user order, [pos(3), quat_xyzw(4)]
quat_xyzw = pose[3:7]
expected_force_w = math_utils.quat_apply(quat_xyzw, force_b[0, 0])
expected_torque_w = math_utils.quat_apply(quat_xyzw, torque_b[0, 0])
torch.testing.assert_close(wrench_buf[0, backend_body_id, 0:3], expected_force_w, rtol=1e-4, atol=1e-4)
torch.testing.assert_close(wrench_buf[0, backend_body_id, 3:6], expected_torque_w, rtol=1e-4, atol=1e-4)
torch.testing.assert_close(wrench_buf[0, backend_body_id, 6:9], pose[0:3], rtol=1e-4, atol=1e-4)
@pytest.mark.parametrize("num_articulations", [1])
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_reversed_joint_ordering_joint_acc_matches_canonicalized_finite_difference(sim, num_articulations, device):
"""Reversed joint ordering: the fused ``joint_acc`` finite difference reads the backend-order
velocity source and equals the identity-order acceleration permuted into public order.
"""
articulation_cfg = generate_articulation_cfg("anymal").replace(
joint_ordering=tuple(reversed(ANYMAL_C_PHYSX_JOINT_NAMES))
)
articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device)
sim.reset()
data = articulation.data
joint_ordering = articulation.joint_ordering
assert joint_ordering is not None
num_joints = articulation.num_joints
user_to_backend = torch.as_tensor(
[int(joint_ordering.user_to_backend_indices[u]) for u in range(num_joints)],
dtype=torch.long,
device=device,
)
# Controlled, non-uniform finite-difference scenario in backend order (so a wrong permutation
# in the kernel would produce a different result -- i.e. the test has teeth).
cur_vel_backend = (torch.arange(1, num_joints + 1, dtype=torch.float32, device=device) * 0.1).reshape(1, -1)
prev_vel_backend = (torch.arange(1, num_joints + 1, dtype=torch.float32, device=device) * -0.03).reshape(1, -1)
# Push the current velocity into the backend DOF_VELOCITY binding (backend order).
articulation.root_view.set_attribute(TT.DOF_VELOCITY, wp.from_torch(cur_vel_backend.contiguous()))
# ``_previous_joint_vel`` is stored in PUBLIC order: prev_user[u] = prev_backend[map[u]].