forked from isaac-sim/IsaacLab
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnewton_manager.py
More file actions
3454 lines (2973 loc) · 158 KB
/
Copy pathnewton_manager.py
File metadata and controls
3454 lines (2973 loc) · 158 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
"""Newton physics manager for Isaac Lab."""
from __future__ import annotations
import contextlib
import ctypes
import gc
import inspect
import logging
import re
from abc import abstractmethod
from collections.abc import Callable, Iterable, Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
import numpy as np
import torch
import warp as wp
# Load CUDA runtime for relaxed-mode graph capture (RTX-compatible).
# cudaStreamCaptureModeRelaxed (2) allows the RTX compositor's background
# CUDA stream to keep running during capture without invalidating it.
try:
_cudart = ctypes.CDLL("libcudart.so.12")
except OSError:
try:
_cudart = ctypes.CDLL("libcudart.so")
except OSError:
_cudart = None
@contextlib.contextmanager
def _paused_gc():
"""Pause Python garbage collection for the duration of a CUDA graph capture.
A garbage-collection pass inside a capture window can drop the last
reference to an array allocated earlier in the capture. While the capture
is paused for a ``wp.capture_while``/``wp.capture_if`` conditional body,
Warp then inserts the memory free node into the body graph with dependency
nodes from the parent graph, which fails and latches a sticky CUDA error
that poisons a later, unrelated copy. Reference-count-driven frees are
deterministic solver behavior and remain allowed; only the collector is
deferred, and a collection runs immediately after the capture window,
where freeing graph-scoped allocations is handled correctly.
"""
was_enabled = gc.isenabled()
gc.disable()
try:
yield
finally:
if was_enabled:
gc.enable()
gc.collect()
from newton import (
Axis,
CollisionPipeline,
Contacts,
Control,
Heightfield,
Model,
ModelBuilder,
ModelFlags,
ShapeFlags,
State,
eval_fk,
)
from newton.selection import ArticulationView
from newton.sensors import SensorContact as NewtonContactSensor
from newton.sensors import SensorFrameTransform
from newton.sensors import SensorIMU as NewtonSensorIMU
from newton.solvers import SolverBase, SolverKamino
from newton.usd import SchemaResolverNewton, SchemaResolverPhysx
from pxr import Usd, UsdGeom
from isaaclab.physics import CallbackHandle, PhysicsEvent, PhysicsManager
from isaaclab.scene_data import SceneDataBackend, SceneDataFormat, SceneDataProvider
from isaaclab.scene_data.deformable_vis_remap import (
VolumeVisRemap,
launch_batch_particle_slice_copy,
launch_batch_volume_vis_remap,
)
from isaaclab.sim import SimulationContext
from isaaclab.sim.utils.newton_model_utils import replace_newton_builder_shape_colors
from isaaclab.sim.utils.queries import has_deformable_curve_api
from isaaclab.sim.utils.stage import get_current_stage
from isaaclab.utils import checked_apply
from isaaclab.utils.string import resolve_matching_names
from isaaclab.utils.timer import Timer
from isaaclab.utils.version import has_kit
from isaaclab.utils.warp.index_kernel import IndexKernelDispatcher
from isaaclab_newton.cloner.newton_clone_utils import (
_restore_visible_colliders_without_visual_shapes,
replicate_builder_mapping,
)
from isaaclab_newton.physics.featherstone_manager_cfg import FeatherstoneSolverCfg
from isaaclab_newton.physics.mjwarp_manager_cfg import MJWarpSolverCfg
from isaaclab_newton.physics.newton_manager_cfg import NewtonCfg, NewtonShapeCfg, NewtonSolverCfg
from isaaclab_newton.physics.visualization_builder import build_visualization_builder_from_stage_envs
from isaaclab_newton.physics.visualization_deformables import populate_shadow_deformable_registry
from isaaclab_newton.physics.xpbd_manager_cfg import XPBDSolverCfg
from isaaclab_newton.renderers.visual_material import (
VisualMaterialWriter,
VisualShapeColorWriter,
import_builder_visual_material_paths,
)
if TYPE_CHECKING:
from isaaclab.actuators.newton import NewtonActuatorAdapter
from isaaclab.assets import BaseArticulation
from isaaclab.renderers.base_renderer import VisualMaterialBatch
from isaaclab_newton.physics.newton_collision_cfg import NewtonCollisionPipelineCfg
def _compile_label_pattern(expr: str | list[str] | None) -> re.Pattern[str] | None:
"""Compile selector expressions for Newton's full label matching."""
if not expr:
return None
return re.compile("|".join((expr,) if isinstance(expr, str) else expr))
logger = logging.getLogger(__name__)
# Tagged union for entries in _cl_site_index_map.
# _GlobalSite: (global_shape_idx, None) — body_pattern was None
# _LocalSite: (None, [[env0_idx, ...], ...]) — per-world site indices
@wp.kernel(enable_backward=False)
def _set_fabric_transforms(
fabric_transforms: wp.fabricarray(dtype=wp.mat44d),
newton_indices: wp.fabricarray(dtype=wp.uint32),
newton_body_q: wp.array(ndim=1, dtype=wp.transformf),
):
"""Write Newton body transforms to Fabric world matrices.
For each Fabric prim at thread ``i``, reads the Newton body transform at
``newton_body_q[newton_indices[i]]`` and stores it as a column-major
``mat44d`` in ``fabric_transforms[i]``.
"""
i = int(wp.tid())
idx = int(newton_indices[i])
transform = newton_body_q[idx]
fabric_transforms[i] = wp.transpose(wp.mat44d(wp.transform_to_matrix(transform)))
@wp.kernel(enable_backward=False)
def _sync_particle_points(
fabric_points: wp.fabricarrayarray(dtype=wp.vec3f),
fabric_world_matrices: wp.fabricarray(dtype=wp.mat44d),
offsets: wp.fabricarray(dtype=wp.uint32),
counts: wp.fabricarray(dtype=wp.uint32),
particle_q: wp.array(dtype=wp.vec3f),
):
"""Write Newton particle positions into Fabric mesh point arrays as local-frame points.
Newton stores particle positions in world space in ``state.particle_q``. The Fabric
``points`` attribute on a ``UsdGeom.Mesh`` is local-space -- Kit multiplies by the
mesh prim's resolved ``omni:fabric:worldMatrix`` at render time.
This kernel inverts the mesh prim's world matrix to convert each world-space particle
position into local-space before writing.
"""
i = wp.tid()
offset = int(offsets[i])
num_points = int(counts[i])
# Un-transpose Fabric's stored matrix to get the standard homogeneous form
world_matrix = wp.transpose(wp.mat44f(fabric_world_matrices[i]))
inv_world_matrix = wp.inverse(world_matrix)
for j in range(num_points):
fabric_points[i][j] = wp.transform_point(inv_world_matrix, particle_q[offset + j])
@wp.kernel(enable_backward=False)
def _sync_cable_points(
fabric_points: wp.fabricarrayarray(dtype=wp.vec3f),
fabric_world_matrices: wp.fabricarray(dtype=wp.mat44d),
offsets: wp.fabricarray(dtype=wp.uint32),
counts: wp.fabricarray(dtype=wp.uint32),
shape_ids: wp.array(dtype=wp.int32),
shape_body: wp.array(dtype=wp.int32),
body_q: wp.array(dtype=wp.transformf),
shape_transform: wp.array(dtype=wp.transformf),
shape_scale: wp.array(dtype=wp.vec3f),
):
"""Write Newton cable segment endpoints into Fabric curve points."""
curve = wp.tid()
offset = int(offsets[curve])
segment_count = int(counts[curve])
world_matrix = wp.transpose(wp.mat44f(fabric_world_matrices[curve]))
world_to_curve = wp.inverse(world_matrix)
for point in range(segment_count + 1):
endpoint_w = wp.vec3f()
if point == 0:
shape = shape_ids[offset]
shape_q = wp.transform_multiply(body_q[shape_body[shape]], shape_transform[shape])
endpoint_w = wp.transform_point(shape_q, wp.vec3f(0.0, 0.0, -shape_scale[shape][1]))
elif point == segment_count:
shape = shape_ids[offset + segment_count - 1]
shape_q = wp.transform_multiply(body_q[shape_body[shape]], shape_transform[shape])
endpoint_w = wp.transform_point(shape_q, wp.vec3f(0.0, 0.0, shape_scale[shape][1]))
else:
left_shape = shape_ids[offset + point - 1]
left_q = wp.transform_multiply(body_q[shape_body[left_shape]], shape_transform[left_shape])
left_w = wp.transform_point(left_q, wp.vec3f(0.0, 0.0, shape_scale[left_shape][1]))
right_shape = shape_ids[offset + point]
right_q = wp.transform_multiply(body_q[shape_body[right_shape]], shape_transform[right_shape])
right_w = wp.transform_point(right_q, wp.vec3f(0.0, 0.0, -shape_scale[right_shape][1]))
endpoint_w = 0.5 * (left_w + right_w)
fabric_points[curve][point] = wp.transform_point(world_to_curve, endpoint_w)
@dataclass
class _ParticleVisualPrim:
"""A ``UsdGeom.Points`` prim mirroring a slice of Newton's particle state."""
points_attr: Usd.Attribute
offset: int
count: int
sync_frequency: int
frames_since_sync: int
@wp.kernel(enable_backward=False)
def _or_reset_masks_from_mask(
env_mask: wp.array(dtype=wp.bool),
articulation_ids: wp.array2d(dtype=int),
world_mask: wp.array(dtype=wp.bool),
fk_mask: wp.array(dtype=wp.bool),
):
"""OR env_mask into world_mask and set corresponding articulation bits in fk_mask."""
world, arti = wp.tid()
if env_mask[world]:
world_mask[world] = True
fk_mask[articulation_ids[world, arti]] = True
@wp.kernel(enable_backward=False)
def _scatter_reset_masks_from_ids(
env_ids: wp.array(dtype=Any),
articulation_ids: wp.array2d(dtype=int),
world_mask: wp.array(dtype=wp.bool),
fk_mask: wp.array(dtype=wp.bool),
):
"""Scatter-set world_mask and fk_mask from sparse env_ids."""
i, arti = wp.tid()
world = wp.int32(env_ids[i])
world_mask[world] = True
fk_mask[articulation_ids[world, arti]] = True
_SCATTER_RESET_MASKS_FROM_IDS_DISPATCHER = IndexKernelDispatcher(_scatter_reset_masks_from_ids, ("env_ids",))
def _scatter_reset_masks_from_ids_kernel(env_ids: wp.array | torch.Tensor) -> wp.Kernel:
"""Select the reset-mask writer matching the environment selector dtype."""
return _SCATTER_RESET_MASKS_FROM_IDS_DISPATCHER.select(env_ids)
@wp.kernel(enable_backward=False)
def _or_world_reset_mask_from_mask(env_mask: wp.array(dtype=wp.bool), world_mask: wp.array(dtype=wp.bool)):
"""Mark masked worlds for solver reset without requesting FK."""
world = wp.tid()
if env_mask[world]:
world_mask[world] = True
@wp.kernel(enable_backward=False)
def _scatter_world_reset_mask_from_ids(env_ids: wp.array(dtype=wp.int32), world_mask: wp.array(dtype=wp.bool)):
"""Mark selected worlds for solver reset without requesting FK."""
world_mask[env_ids[wp.tid()]] = True
class NewtonSceneDataBackend(SceneDataBackend):
"""Scene data backend that reads rigid body transforms from Newton's simulation state.
The backend reads ``body_q`` (an array of :class:`wp.transformf`) from
Newton's current state and exposes it as :class:`SceneDataFormat.Transform`.
Body paths come from the model's ``body_label`` attribute.
"""
def __init__(self):
self._scene_data = SceneDataFormat.Transform()
@property
def transforms(self) -> SceneDataFormat.Transform:
"""Return the current Newton rigid body transforms as :class:`SceneDataFormat.Transform`."""
self._scene_data.transforms = self.state.body_q
return self._scene_data
@property
def transform_count(self) -> int:
"""Return the number of rigid body transforms in the Newton sim."""
return self.model.body_count
@property
def transform_paths(self) -> list[str]:
"""Return the prim paths for each rigid body transform."""
if self.model.body_label is not None:
return list(self.model.body_label)
return []
@property
def model(self) -> Model:
return NewtonManager.get_model()
@property
def state(self) -> State:
"""Return Newton state after applying pending forward kinematics."""
return NewtonManager.get_state()
def _eval_fk_unbound(world_reset_mask: wp.array | None, fk_mask: wp.array | None) -> None:
"""Default :attr:`NewtonManager._eval_fk` value before a solver is initialized.
Raises so a stray ``forward()`` / ``step()`` before ``initialize_solver()`` fails loudly
instead of silently running a wrong (or no) FK.
"""
raise RuntimeError(
"FK hook is not bound. NewtonManager.initialize_solver() must run "
"(via reset()) before forward()/step() can run forward kinematics."
)
def _reset_solver_internals_unbound(world_mask: wp.array | None) -> None:
"""Default reset-hook delegate value before a solver is initialized."""
raise RuntimeError(
"Solver reset hook is not bound. NewtonManager.initialize_solver() must run "
"(via reset()) before forward()/step() can reset solver internals."
)
class NewtonManager(PhysicsManager):
"""Abstract Newton physics manager for Isaac Lab.
Class-level (singleton-like) manager that owns simulation lifecycle, model
state, contacts/collision pipeline, sensors, replication, and CUDA-graph
orchestration.
Concrete subclasses (one per solver) implement :meth:`_build_solver` and
may extend :meth:`_initialize_contacts`, :meth:`_prepare_builder_for_finalize`,
:meth:`_step_solver`, :meth:`_supports_cuda_graph_capture`,
:meth:`_requires_initial_reset_before_graph_capture`,
:meth:`_reset_solver_internals`,
:meth:`_solver_specific_clear`, :meth:`_check_solver_status`, and
:meth:`_log_solver_debug`.
Subclasses are selected via :attr:`NewtonSolverCfg.class_type`, which
:meth:`NewtonCfg.__post_init__` propagates onto :attr:`NewtonCfg.class_type`
so that ``SimulationContext`` resolves the matching subclass automatically.
Lifecycle: ``initialize() -> reset() -> step()`` (repeated) ``-> close()``.
.. note::
Shared state lives on :class:`NewtonManager` (the base) by design — the
framework imports ``NewtonManager`` directly and reads attributes such
as ``_model`` / ``_state_0`` / ``_builder`` from many places. Lifecycle
methods therefore assign through the explicit base class
(``NewtonManager._foo = ...``) rather than through ``cls`` so that the
canonical state remains discoverable from external readers regardless of
which subclass is active.
"""
_solver_dt: float = 1.0 / 200.0
_num_substeps: int = 1
_decimation: int = 1
_collision_decimation: int = 0
_deterministic_mode: wp.DeterministicMode = wp.DeterministicMode.NOT_GUARANTEED
_num_envs: int | None = None
_supports_rigid_body_force_input: bool = False
"""Whether the solver consumes applied rigid-body forces from :class:`State`."""
# Newton model and state
_builder: ModelBuilder = None
_model: Model = None
_solver: SolverBase | None = None
_use_single_state: bool | None = None
"""Use only one state for both input and output for solver stepping. Requires solver support."""
_state_0: State = None
_state_1: State = None
_control: Control = None
# Physics settings
_gravity_vector: tuple[float, float, float] = (0.0, 0.0, -9.81)
_up_axis: str = "Z"
# Collision and contacts
_contacts: Contacts | None = None
_needs_collision_pipeline: bool = False
_collision_pipeline = None
_collision_cfg: NewtonCollisionPipelineCfg | None = None
_newton_contact_sensors: dict = {} # Maps sensor_key to NewtonContactSensor
_newton_frame_transform_sensors: list = [] # List of SensorFrameTransform
_newton_imu_sensors: list = [] # List of NewtonSensorIMU
_pending_extended_state_attributes: set[str] = set()
_pending_extended_contact_attributes: set[str] = set()
_report_contacts: bool = False
_supports_contact_sensors: bool = True
# Per-world reset masks (allocated in start_simulation, consumed in step/forward).
# Newton reserves the final slot for global entities in world -1.
_world_reset_mask: wp.array | None = None # (num_envs + 1,) wp.bool
_fk_reset_mask: wp.array | None = None # (articulation_count,) wp.bool — for eval_fk(mask=...)
# Solver-specialized FK delegate. Bound in initialize_solver() to the active subclass's choice of FK implementation.
_eval_fk: Callable[[wp.array | None, wp.array | None], None] = _eval_fk_unbound
# Solver-specialized reset delegate. Like _eval_fk, this must dispatch correctly through the base manager.
_reset_solver_internals_delegate: Callable[[wp.array | None], None] = _reset_solver_internals_unbound
# Newton actuator adapter (owns actuators and double-buffered states)
_adapter: NewtonActuatorAdapter | None = None
# In-graph hooks invoked after the actuator step and before the solver
# substeps, in registration order. Multiple articulations register their
# implicit-DOF telemetry / FF-routing kernels here.
_post_actuator_callbacks: list[Callable[[], None]] = []
# In-graph hooks invoked immediately before every solver substep.
_state_force_callbacks: list[Callable[[State], None]] = []
# In-graph hooks invoked after the last solver substep and before sensors,
# in registration order. Articulations with non-identity ordering register
# their backend-to-user state republish kernels here so the reorders are
# recorded into every captured graph.
_post_step_callbacks: list[Callable[[], None]] = []
# CUDA graphing
_graph = None
_graph_capture_pending: bool = False
# Newton scene-query scheduling and graph execution.
_sensor_tasks: dict[str, Callable[[], None]] = {}
_sensor_graph: wp.Graph | None = None
_sensor_flags: wp.array | None = None
_sensor_flags_host: np.ndarray | None = None
_sensor_state: State | None = None
_sensor_state_dirty: bool = True
_sensor_graph_capture_failed: bool = False
_sensor_bvh_shape_flags: ShapeFlags = ShapeFlags.VISIBLE
# USD/Fabric sync
_newton_stage_path = None
_usdrt_stage = None
_newton_index_attr = "newton:index"
_clone_physics_only = False
_transforms_dirty: bool = False
_transforms_may_change_on_graph_replay: bool = False
_particles_dirty: bool = False
_cables_dirty: bool = False
_newton_cable_offset_attr = "newton:cableOffset"
_newton_cable_count_attr = "newton:cableSegmentCount"
_cable_shape_ids: wp.array | None = None
_cable_sync_cpu_buffers: tuple[wp.array, ...] | None = None
_newton_particle_offset_attr = "newton:particleOffset"
_newton_particle_count_attr = "newton:particleCount"
_particle_visual_prims: dict[str, _ParticleVisualPrim] = {}
# Cached after the first fabric sync that probes IFabricHierarchy GPU APIs.
_use_fabric_gpu_hierarchy: bool | None = None
# Set to True after sync_transforms_to_usd() successfully writes body positions for
# the first time in each simulation session. Reset to False in clear(). Polled by
# test drain helpers to know when the GPU has propagated the newton:index Fabric
# attribute and body_q values are valid.
_newton_fabric_ready: bool = False
# Model changes (callbacks use unified system from PhysicsManager)
_model_changes: set[int] = set()
# Scene data backend
_scene_data_backend: NewtonSceneDataBackend | None = None
# Visualization-only state used when the sim backend is PhysX. Populated
# lazily in :meth:`_ensure_visualization_model` and updated each render
# frame in :meth:`update_visualization_state`.
_scene_data: SceneDataFormat.Transform | None = None
_scene_data_mapping: wp.array | None = None
_scene_data_points: SceneDataFormat.Points | None = None
_scene_data_geometry_mapping: wp.array | None = None
_shadow_deformable_entities: list | None = None
_sim_particle_q: wp.array | None = None
_mapped_sim_particle_offsets: set[int] | None = None
_shadow_deformable_sync_skip_warned: set[str] = set()
_shadow_deformable_remap_batches: list | None = None
_shadow_deformable_copy_batch: tuple | None = None
_shadow_deformable_batch_sync_key: tuple | None = None
_visualization_stop_callback: CallbackHandle | None = None
_builder_attribute_solvers: tuple[type[SolverBase], ...] = ()
_mpm_object_registry: list = []
# CL: Cloning / Replication logic
# TODO: These attributes support cloning-specific logic and should be moved into a cloner class
# Pending site requests from sensors.
# Key: (body_pattern, per_world, xform_floats), Value: (label, wp.transform)
# identical (body_pattern, per_world, transform) reuses the same site.
_cl_pending_sites: dict[tuple[str | None, bool, tuple[float, ...]], tuple[str, wp.transform]] = {}
# Maps each site label to its resolved global or local site entry.
_GlobalSite = tuple[int, None]
_LocalSite = tuple[None, list[list[int]]]
_SiteEntry = _GlobalSite | _LocalSite
_cl_site_index_map: dict[str, _SiteEntry] = {}
_cl_fabric_body_bindings: list[tuple[str, int]] | None = None
_world_xforms: list[wp.transform] | None = None
# Per-source builders retained from replication, keyed by clone-plan source
# path. Single-model consumers (e.g. batched Newton IK) finalize a single-env
# model from these and resolve it via ``query.path_to_source``.
_cl_protos: dict[str, ModelBuilder] = {}
_deformable_registry: list = []
_per_world_builder_hooks: list[Callable[[ModelBuilder, int, list[float], list[float]], None]] = []
@classmethod
def initialize(cls, sim_context: SimulationContext) -> None:
"""Initialize the manager with simulation context.
Args:
sim_context: Parent simulation context.
"""
super().initialize(sim_context)
# Newton-specific setup: get gravity from SimulationCfg (not physics manager cfg)
sim = PhysicsManager._sim
if sim is not None:
NewtonManager._gravity_vector = sim.cfg.gravity # type: ignore[union-attr]
# USD/Fabric sync for Omniverse rendering (visualizer) or Newton+RTX (Kit cameras)
try:
requested = sim.resolve_visualizer_types()
except Exception:
requested = []
viz_raw = sim.get_setting("/isaaclab/visualizer/types")
if isinstance(viz_raw, str):
requested = [v for part in viz_raw.split(",") for v in part.split() if v]
from isaaclab.app.settings_manager import get_settings_manager
cameras_enabled = bool(get_settings_manager().get("/isaaclab/cameras_enabled", False))
cls._clone_physics_only = not has_kit() or ("kit" not in requested and not cameras_enabled)
cls._scene_data_backend = NewtonSceneDataBackend()
@classmethod
def reset(cls, soft: bool = False) -> None:
"""Reset physics simulation.
A hard reset (``soft=False``) re-finalizes the Newton model, reallocating
its device arrays. The cached collision pipeline, contacts and any
captured CUDA graph reference the old buffers, so they are released here
and rebuilt against the re-finalized model by :meth:`initialize_solver`.
This avoids the illegal CUDA memory access (CUDA error 700) that would
otherwise occur on the first step after a hard reset.
A soft reset (``soft=True``) skips this full reinitialization and reuses
the existing model, solver, collision pipeline and CUDA graph.
Args:
soft: If True, skip full reinitialization.
"""
if not soft:
# Release the cached collision pipeline, contacts and CUDA graph;
# they point at the old model's freed buffers (CUDA 700 on next step).
NewtonManager._graph = None
NewtonManager._graph_capture_pending = False
NewtonManager._collision_pipeline = None
NewtonManager._contacts = None
cls.start_simulation()
cls.initialize_solver()
@classmethod
def _eval_fk_impl(cls, world_reset_mask: wp.array | None, fk_mask: wp.array | None) -> None:
"""Update body states from joint coordinates.
Solver-specialized FK implementation. The base implementation runs Newton's generic
``eval_fk`` over the articulations selected by ``fk_mask``. Subclasses may override
this method to use a solver-specific FK.
Args:
world_reset_mask: Per-world mask of environments to reset (``None`` means all).
Unused by the base implementation; consumed by solver-specific overrides such as
:meth:`NewtonKaminoManager._eval_fk_impl`.
fk_mask: Per-articulation mask of articulations to update (``None`` means all).
"""
eval_fk(cls._model, cls._state_0.joint_q, cls._state_0.joint_qd, cls._state_0, fk_mask)
@classmethod
def forward(cls) -> None:
"""Update articulation kinematics without stepping physics.
Update body poses from joint coordinates via the solver-specialized FK delegate
(:attr:`_eval_fk`, bound to the active subclass's :meth:`_eval_fk_impl` in
:meth:`initialize_solver`). Only the articulations flagged dirty in
:attr:`_fk_reset_mask` and :attr:`_world_reset_mask` (see :meth:`invalidate_fk`) are
updated. The masks are consumed (zeroed) afterwards so the next :meth:`step` does not
redundantly re-solve them.
The delegate (rather than a direct ``cls._eval_fk_impl`` call) is required because the
data layer invokes ``NewtonManager.forward()`` on the base class, where ``cls`` is the
base ``NewtonManager``; the bound delegate dispatches to the concrete subclass override.
"""
cls._reset_solver_internals_delegate(cls._world_reset_mask)
cls._eval_fk(cls._world_reset_mask, cls._fk_reset_mask)
if cls._fk_reset_mask is not None:
cls._fk_reset_mask.zero_()
if cls._world_reset_mask is not None:
cls._world_reset_mask.zero_()
cls._mark_sensor_state_dirty()
@classmethod
def video_capture_backend(cls) -> str:
"""Newton GL headless perspective video capture."""
return "newton_gl"
@classmethod
def pre_render(cls) -> None:
"""Refresh derived Newton state before cameras and visualizers read it."""
if cls._fk_reset_mask is not None:
cls.forward()
if NewtonManager._transforms_may_change_on_graph_replay:
cls._mark_transforms_dirty()
cls.sync_transforms_to_usd()
cls.sync_cables_to_usd()
cls.sync_particles_to_usd()
@classmethod
def sync_transforms_to_usd(cls) -> None:
"""Write Newton body_q to USD Fabric world matrices for Kit viewport / RTX rendering.
No-op when ``_usdrt_stage`` is None (i.e. Kit visualizer is not active)
or when transforms have not changed since the last sync.
Called at render cadence by :meth:`pre_render` (via
:meth:`~isaaclab.sim.SimulationContext.render`).
Physics stepping marks transforms dirty via :meth:`_mark_transforms_dirty`
so that the expensive Fabric hierarchy update only runs once per render
frame rather than after every physics step.
Uses ``wp.fabricarray`` directly (no ``isaacsim.physics.newton`` extension needed).
The Warp kernel reads ``state_0.body_q[newton_index[i]]`` and writes the
corresponding ``mat44d`` to ``omni:fabric:worldMatrix`` for each prim.
When ``IFabricHierarchy.update_world_xforms_gpu_with_options`` is
available the method mirrors PhysX's ``DirectGpuHelper`` pattern: pause
Fabric change tracking, write transforms, resume tracking, then run the
GPU hierarchy update with ``RIGID_BODY | FORCE_UPDATE`` so Newton-authored
world matrices stay authoritative on rigid-body prims. Otherwise it
falls back to the CPU ``update_world_xforms()`` path.
"""
if cls._usdrt_stage is None or cls._model is None or cls._state_0 is None:
return
if not cls._transforms_dirty:
return
try:
import usdrt
fabric_hierarchy = None
gpu_opts_cls = None
if hasattr(usdrt, "hierarchy"):
fabric_hierarchy = usdrt.hierarchy.IFabricHierarchy().get_fabric_hierarchy(
cls._usdrt_stage.GetFabricId(), cls._usdrt_stage.GetStageIdAsStageId()
)
gpu_opts_cls = getattr(usdrt.hierarchy, "FabricHierarchyGpuUpdateOptions", None)
if cls._use_fabric_gpu_hierarchy is None and hasattr(usdrt, "hierarchy"):
# Probe the pybind class once so a transient null hierarchy handle does
# not permanently disable the GPU path for the session.
NewtonManager._use_fabric_gpu_hierarchy = gpu_opts_cls is not None and hasattr(
usdrt.hierarchy.IFabricHierarchy, "update_world_xforms_gpu_with_options"
)
if cls._use_fabric_gpu_hierarchy:
logger.info("Fabric GPU transform hierarchy enabled via IFabricHierarchy")
else:
logger.info("Fabric GPU transform hierarchy unavailable; falling back to update_world_xforms()")
use_gpu_hierarchy = bool(
cls._use_fabric_gpu_hierarchy and fabric_hierarchy is not None and gpu_opts_cls is not None
)
# Pause hierarchy change tracking BEFORE SelectPrims.
# SelectPrims with ReadWrite access calls getAttributeArrayGpu
# internally, which marks Fabric buffers dirty. If tracking is
# still active at that point the hierarchy records the change and
# Kit's updateWorldXforms will do an expensive connectivity
# rebuild every frame. PhysX avoids this via ScopedUSDRT which
# pauses tracking before any Fabric writes.
if use_gpu_hierarchy:
fabric_hierarchy.track_world_xform_changes(False)
fabric_hierarchy.track_local_xform_changes(False)
try:
selection = cls._usdrt_stage.SelectPrims(
require_attrs=[
(usdrt.Sdf.ValueTypeNames.Matrix4d, "omni:fabric:worldMatrix", usdrt.Usd.Access.ReadWrite),
(usdrt.Sdf.ValueTypeNames.UInt, cls._newton_index_attr, usdrt.Usd.Access.Read),
],
device=str(PhysicsManager._device),
)
if selection.GetCount() == 0:
# The newton:index attribute is written CPU-side by start_simulation() but
# GPU propagation is deferred. Keep _transforms_dirty=True so the next
# pre_render() retries once initialize_solver() has completed (FK delegate
# bound) and body_q holds valid values.
if cls._eval_fk is _eval_fk_unbound:
NewtonManager._transforms_dirty = False
return
fabric_transforms = wp.fabricarray(selection, "omni:fabric:worldMatrix")
newton_indices = wp.fabricarray(selection, cls._newton_index_attr)
wp.launch(
_set_fabric_transforms,
dim=newton_indices.shape[0],
inputs=[fabric_transforms, newton_indices, cls._state_0.body_q],
device=PhysicsManager._device,
)
wp.synchronize_device(PhysicsManager._device)
NewtonManager._newton_fabric_ready = True
NewtonManager._transforms_dirty = False
if use_gpu_hierarchy:
# RIGID_BODY: inverse-propagate on PhysicsRigidBodyAPI buckets
# (keep Newton world matrices, derive local). FORCE_UPDATE:
# bypass the change-listener dirty check after tracking pause.
fabric_hierarchy.update_world_xforms_gpu_with_options(
gpu_opts_cls.RIGID_BODY | gpu_opts_cls.FORCE_UPDATE
)
elif fabric_hierarchy is not None:
fabric_hierarchy.update_world_xforms()
finally:
if use_gpu_hierarchy:
fabric_hierarchy.track_world_xform_changes(True)
fabric_hierarchy.track_local_xform_changes(True)
except Exception:
logger.exception("[NewtonManager] sync_transforms_to_usd FAILED")
@classmethod
def sync_cables_to_usd(cls) -> None:
"""Write Newton cable segment endpoints to Fabric curve points."""
if not cls._cables_dirty:
return
if cls._usdrt_stage is None or cls._cable_shape_ids is None:
NewtonManager._cables_dirty = False
return
try:
import usdrt # noqa: PLC0415
selection = cls._usdrt_stage.SelectPrims(
require_attrs=[
(usdrt.Sdf.ValueTypeNames.Point3fArray, "points", usdrt.Usd.Access.ReadWrite),
(usdrt.Sdf.ValueTypeNames.UInt, cls._newton_cable_offset_attr, usdrt.Usd.Access.Read),
(usdrt.Sdf.ValueTypeNames.UInt, cls._newton_cable_count_attr, usdrt.Usd.Access.Read),
(usdrt.Sdf.ValueTypeNames.Matrix4d, "omni:fabric:worldMatrix", usdrt.Usd.Access.Read),
],
device="cpu",
)
if selection.GetCount() == 0:
NewtonManager._cables_dirty = False
return
_, _, body_q, _, _ = cls._cable_sync_cpu_buffers
wp.copy(body_q, cls._state_0.body_q)
wp.launch(
_sync_cable_points,
dim=selection.GetCount(),
inputs=[
wp.fabricarrayarray(data=selection, attrib="points", dtype=wp.vec3f),
wp.fabricarray(data=selection, attrib="omni:fabric:worldMatrix"),
wp.fabricarray(data=selection, attrib=cls._newton_cable_offset_attr),
wp.fabricarray(data=selection, attrib=cls._newton_cable_count_attr),
*cls._cable_sync_cpu_buffers,
],
device="cpu",
)
NewtonManager._cables_dirty = False
except Exception:
logger.exception("[NewtonManager] sync_cables_to_usd FAILED")
@classmethod
def sync_particles_to_usd(cls) -> None:
"""Write Newton particle positions to USD/Fabric for USD-stage rendering.
Two prim families are synced from ``state_0.particle_q``:
* Fabric mesh prims tagged with ``newton:particleOffset`` /
``newton:particleCount`` (deformable visual meshes) receive
local-frame points on the GPU via :meth:`_sync_fabric_mesh_particles`.
* ``UsdGeom.Points`` prims registered through
:meth:`register_particle_visual_prim` (MPM particle clouds) receive
world-frame points via :meth:`_sync_particle_points_prims`.
No-op when there is no particle state or nothing changed since the
last sync.
"""
if not cls._particles_dirty or cls._state_0 is None or cls._state_0.particle_q is None:
return
try:
cls._sync_fabric_mesh_particles()
NewtonManager._particles_dirty = cls._sync_particle_points_prims()
except Exception:
logger.exception("[NewtonManager] sync_particles_to_usd FAILED")
@classmethod
def _sync_fabric_mesh_particles(cls) -> None:
"""Write ``state_0.particle_q`` into Fabric mesh point arrays as local-frame points."""
if cls._usdrt_stage is None:
return
import usdrt # noqa: PLC0415
selection = cls._usdrt_stage.SelectPrims(
require_attrs=[
(usdrt.Sdf.ValueTypeNames.Point3fArray, "points", usdrt.Usd.Access.ReadWrite),
(usdrt.Sdf.ValueTypeNames.UInt, cls._newton_particle_offset_attr, usdrt.Usd.Access.Read),
(usdrt.Sdf.ValueTypeNames.UInt, cls._newton_particle_count_attr, usdrt.Usd.Access.Read),
(usdrt.Sdf.ValueTypeNames.Matrix4d, "omni:fabric:worldMatrix", usdrt.Usd.Access.Read),
],
device=str(PhysicsManager._device),
)
if selection.GetCount() == 0:
return
wp.launch(
_sync_particle_points,
dim=selection.GetCount(),
inputs=[
wp.fabricarrayarray(data=selection, attrib="points", dtype=wp.vec3f),
wp.fabricarray(data=selection, attrib="omni:fabric:worldMatrix"),
wp.fabricarray(data=selection, attrib=cls._newton_particle_offset_attr),
wp.fabricarray(data=selection, attrib=cls._newton_particle_count_attr),
cls._state_0.particle_q,
],
device=PhysicsManager._device,
)
@classmethod
def _sync_particle_points_prims(cls) -> bool:
"""Write registered ``UsdGeom.Points`` prims; return ``True`` while throttled prims remain."""
if not cls._particle_visual_prims:
return False
due = []
for record in cls._particle_visual_prims.values():
record.frames_since_sync += 1
if record.frames_since_sync >= record.sync_frequency:
record.frames_since_sync = 0
due.append(record)
if due:
from pxr import Sdf, Vt # noqa: PLC0415
particle_q = cls._state_0.particle_q.numpy()
with Sdf.ChangeBlock():
for record in due:
points = particle_q[record.offset : record.offset + record.count]
record.points_attr.Set(Vt.Vec3fArray.FromNumpy(points))
return len(due) < len(cls._particle_visual_prims)
@classmethod
def _mark_transforms_dirty(cls) -> None:
"""Flag that rigid-body transforms have changed and Fabric needs re-sync.
The actual sync is deferred to :meth:`sync_transforms_to_usd`,
which runs at render cadence via :meth:`pre_render`.
"""
NewtonManager._transforms_dirty = True
NewtonManager._cables_dirty = True
device = PhysicsManager._device
if device is not None:
device = wp.get_device(device)
if device.is_cuda and device.stream.is_capturing:
NewtonManager._transforms_may_change_on_graph_replay = True
@classmethod
def _mark_particles_dirty(cls) -> None:
"""Flag that particle positions have changed and Fabric needs re-sync.
The actual sync is deferred to the particle sync callback (if registered),
which runs at render cadence via :meth:`pre_render`.
"""
NewtonManager._particles_dirty = True
@classmethod
def _mark_state_dirty(cls) -> None:
"""Flag that all physics state has changed and Fabric needs re-sync.
Convenience method that marks both transforms and particles dirty.
Called by :meth:`_simulate` after stepping.
"""
cls._mark_transforms_dirty()
cls._mark_particles_dirty()
@classmethod
def register_particle_visual_prim(
cls, prim_path: str, particle_offset: int, particle_count: int, sync_frequency: int = 1
) -> None:
"""Register a ``UsdGeom.Points`` prim whose points mirror a slice of Newton's particle state.
Args:
prim_path: Stage path of an existing ``UsdGeom.Points`` prim.
particle_offset: First index of the prim's slice in ``state.particle_q``.
particle_count: Number of particles in the slice.
sync_frequency: Sync the prim every N dirty render frames.
"""
from pxr import UsdGeom # noqa: PLC0415
prim = get_current_stage().GetPrimAtPath(prim_path)
NewtonManager._particle_visual_prims[prim_path] = _ParticleVisualPrim(
points_attr=UsdGeom.Points(prim).GetPointsAttr(),
offset=int(particle_offset),
count=int(particle_count),
sync_frequency=int(sync_frequency),
frames_since_sync=int(sync_frequency),
)
@classmethod
def step(cls) -> None:
"""Step the physics simulation.
The stepping logic follows one of two paths depending on whether
**all** actuators are CUDA-graph-safe:
**All-graphable path** (:meth:`_simulate_full`):
Actuators and solver substeps are captured together in a single
CUDA graph containing the full
``decimation x (actuators + solver substeps)`` loop.
**Eager-actuator path** (fallback, some actuators not graph-safe):
Actuators are stepped eagerly on the CPU timeline (outside the
graph), then a graph containing only the solver substeps is
launched via :meth:`_simulate_physics_only`.
In both paths the sequence within one physics step is::
zero actuated DOFs in control.joint_f
-> actuator.step (computes effort, writes to control.joint_f)
-> solver.step x num_substeps (integrates, reads control.joint_f)
-> sensors.update
"""
sim = PhysicsManager._sim
if sim is None or not sim.is_playing():
return
cls._reset_solver_internals_delegate(cls._world_reset_mask)
# Notify solver of model changes
if cls._model_changes:
with wp.ScopedDevice(PhysicsManager._device):
for change in cls._model_changes:
cls._solver.notify_model_changed(change)
NewtonManager._model_changes = set()
# Lazy CUDA graph capture
cfg = PhysicsManager._cfg
device = PhysicsManager._device
capture_pending = cls._graph_capture_pending and cfg is not None and cfg.use_cuda_graph and "cuda" in device # type: ignore[union-attr]
state_reconciled = False
if capture_pending and cls._usdrt_stage is None:
# Reconcile reset-authored solver resources before standard capture.
cls.forward()
state_reconciled = True
if capture_pending:
NewtonManager._graph_capture_pending = False
if cls._usdrt_stage is None:
simulate = cls._simulate_full if cls._is_all_graphable() else cls._simulate_physics_only
with Timer(name="newton_cuda_graph", msg="CUDA graph took:"):
with _paused_gc(), wp.ScopedCapture(device=device, force_module_load=False) as capture:
simulate()
NewtonManager._graph = capture.graph
logger.info("Newton CUDA graph captured (deferred standard mode)")
else:
NewtonManager._graph = cls._capture_relaxed_graph(device)
if cls._graph is not None:
# Kamino: StateKamino.from_newton() lazily allocates body_f_total,
# joint_q_prev, and joint_lambdas via wp.clone/wp.zeros during the
# first step() inside graph capture. Replay once to pin those
# memory-pool addresses before any eager solver.reset() call.
if isinstance(cls._solver, SolverKamino):
wp.capture_launch(cls._graph)
logger.info("Newton CUDA graph captured (deferred relaxed mode, RTX-compatible)")
else:
logger.warning("Newton deferred CUDA graph capture failed; using eager execution")
# Reconcile authored state after any mutating graph warmup and before the requested physics step.
if not state_reconciled:
cls.forward()
physics_dt = cls._solver_dt * cls._num_substeps
use_graph = cfg is not None and cfg.use_cuda_graph and cls._graph is not None and "cuda" in device # type: ignore[union-attr]
if cls._is_all_graphable():
# --- All actuators are graph-safe: actuators + solver in one graph ---
if use_graph:
wp.capture_launch(cls._graph)