3030from newton ._src .usd .schemas import SchemaResolverNewton , SchemaResolverPhysx
3131from newton .sensors import SensorContact as NewtonContactSensor
3232from newton .sensors import SensorFrameTransform
33+ from newton .sensors import SensorIMU as NewtonSensorIMU
3334from newton .solvers import SolverBase , SolverFeatherstone , SolverMuJoCo , SolverNotifyFlags , SolverXPBD
3435
3536from isaaclab .physics import PhysicsEvent , PhysicsManager
@@ -102,6 +103,9 @@ class NewtonManager(PhysicsManager):
102103 _collision_cfg : NewtonCollisionPipelineCfg | None = None
103104 _newton_contact_sensors : dict = {} # Maps sensor_key to NewtonContactSensor
104105 _newton_frame_transform_sensors : list = [] # List of SensorFrameTransform
106+ _newton_imu_sensors : list = [] # List of NewtonSensorIMU
107+ _pending_extended_state_attributes : set [str ] = set ()
108+ _pending_extended_contact_attributes : set [str ] = set ()
105109 _report_contacts : bool = False
106110 _fk_dirty : bool = False
107111
@@ -391,6 +395,7 @@ def clear(cls):
391395 cls ._collision_cfg = None
392396 cls ._newton_contact_sensors = {}
393397 cls ._newton_frame_transform_sensors = []
398+ cls ._newton_imu_sensors = []
394399 cls ._report_contacts = False
395400 cls ._fk_dirty = False
396401 cls ._graph = None
@@ -402,6 +407,8 @@ def clear(cls):
402407 cls ._model_changes = set ()
403408 cls ._cl_pending_sites = {}
404409 cls ._cl_site_index_map = {}
410+ cls ._pending_extended_state_attributes = set ()
411+ cls ._pending_extended_contact_attributes = set ()
405412 cls ._views = []
406413
407414 @classmethod
@@ -458,6 +465,32 @@ def cl_register_site(cls, body_pattern: str | None, xform: wp.transform) -> str:
458465 cls ._cl_pending_sites [key ] = (label , xform )
459466 return label
460467
468+ @classmethod
469+ def request_extended_state_attribute (cls , attr : str ) -> None :
470+ """Request an extended state attribute (e.g. ``"body_qdd"``).
471+
472+ Sensors call this during ``__init__``, before model finalization.
473+ Attributes are forwarded to the builder in :meth:`start_simulation`
474+ so that subsequent ``model.state()`` calls allocate them.
475+
476+ Args:
477+ attr: State attribute name (must be in ``State.EXTENDED_ATTRIBUTES``).
478+ """
479+ cls ._pending_extended_state_attributes .add (attr )
480+
481+ @classmethod
482+ def request_extended_contact_attribute (cls , attr : str ) -> None :
483+ """Request an extended contact attribute (e.g. ``"force"``).
484+
485+ Sensors call this during ``__init__``, before model finalization.
486+ Attributes are forwarded to the model in :meth:`start_simulation`
487+ so that subsequent ``Contacts`` creation includes them.
488+
489+ Args:
490+ attr: Contact attribute name.
491+ """
492+ cls ._pending_extended_contact_attributes .add (attr )
493+
461494 @classmethod
462495 def _cl_inject_sites (
463496 cls ,
@@ -598,11 +631,18 @@ def start_simulation(cls) -> None:
598631 device = PhysicsManager ._device
599632 logger .info (f"Finalizing model on device: { device } " )
600633 cls ._builder .up_axis = Axis .from_string (cls ._up_axis )
634+ # Forward pending extended attribute requests to builder and clear them
635+ if cls ._pending_extended_state_attributes :
636+ cls ._builder .request_state_attributes (* cls ._pending_extended_state_attributes )
637+ cls ._pending_extended_state_attributes = set ()
601638 with Timer (name = "newton_finalize_builder" , msg = "Finalize builder took:" ):
602639 cls ._model = cls ._builder .finalize (device = device )
603640 cls ._model .set_gravity (cls ._gravity_vector )
604641 cls ._model .num_envs = cls ._num_envs
605642
643+ if cls ._pending_extended_contact_attributes :
644+ cls ._model .request_contact_attributes (* cls ._pending_extended_contact_attributes )
645+ cls ._pending_extended_contact_attributes = set ()
606646 cls ._state_0 = cls ._model .state ()
607647 cls ._state_1 = cls ._model .state ()
608648 cls ._control = cls ._model .control ()
@@ -1010,6 +1050,11 @@ def step_fn(state_0, state_1):
10101050 for sensor in cls ._newton_frame_transform_sensors :
10111051 sensor .update (cls ._state_0 )
10121052
1053+ # Update IMU sensors
1054+ if cls ._newton_imu_sensors :
1055+ for sensor in cls ._newton_imu_sensors :
1056+ sensor .update (cls ._state_0 )
1057+
10131058 # Populate contacts for contact sensors
10141059 if cls ._report_contacts :
10151060 eval_contacts = contacts if contacts is not None else cls ._contacts
@@ -1183,3 +1228,28 @@ def add_frame_transform_sensor(cls, shapes: list[int], reference_sites: list[int
11831228 cls ._newton_frame_transform_sensors .append (sensor )
11841229 logger .info (f"Added frame transform sensor (index={ idx } , shapes={ len (shapes )} )" )
11851230 return idx
1231+
1232+ @classmethod
1233+ def add_imu_sensor (cls , sites : list [int ]) -> int :
1234+ """Add an IMU sensor for measuring acceleration and angular velocity at sites.
1235+
1236+ Creates a ``newton.sensors.SensorIMU`` from pre-resolved site indices,
1237+ appends it to the internal list, and returns its index.
1238+
1239+ Args:
1240+ sites: Ordered list of site indices (one per environment).
1241+
1242+ Returns:
1243+ Index of the newly created sensor in the internal IMU sensor list.
1244+ """
1245+ if cls ._model is None :
1246+ raise RuntimeError ("add_imu_sensor called before model finalization (start_simulation)." )
1247+ sensor = NewtonSensorIMU (
1248+ cls ._model ,
1249+ sites = sites ,
1250+ request_state_attributes = False , # Already requested via NewtonManager
1251+ )
1252+ idx = len (cls ._newton_imu_sensors )
1253+ cls ._newton_imu_sensors .append (sensor )
1254+ logger .info (f"Added IMU sensor (index={ idx } , sites={ len (sites )} )" )
1255+ return idx
0 commit comments