diff --git a/.gitattributes b/.gitattributes index 99e35795611a..5c27e0d2094a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,11 +7,15 @@ *.obj filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text *.mp4 filter=lfs diff=lfs merge=lfs -text +*.webp filter=lfs diff=lfs merge=lfs -text *.pt filter=lfs diff=lfs merge=lfs -text *.jit filter=lfs diff=lfs merge=lfs -text *.hdf5 filter=lfs diff=lfs merge=lfs -text source/isaaclab_tasks/test/golden_images/**/*.png filter=lfs diff=lfs merge=lfs -text +# Generated actuator plots are reviewed as rendered images rather than XML. +docs/source/_static/actuators/*.png binary linguist-generated + *.bat text eol=crlf *.sh text eol=lf diff --git a/docs/index.rst b/docs/index.rst index ac43b901eccd..f99c2daae59a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -99,6 +99,7 @@ Table of Contents :caption: Concepts source/concepts/backends_and_presets + source/concepts/actuators .. toctree:: diff --git a/docs/source/_static/actuators/armature-clip.webp b/docs/source/_static/actuators/armature-clip.webp new file mode 100644 index 000000000000..b20c44e2034f --- /dev/null +++ b/docs/source/_static/actuators/armature-clip.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b2633b6dbdde0e47a95dde8f6f06214ffc3d0786408baeaa9b1f74c6550d5a7 +size 471702 diff --git a/docs/source/_static/actuators/armature-curve-dark.png b/docs/source/_static/actuators/armature-curve-dark.png new file mode 100644 index 000000000000..915f9e55a3f6 Binary files /dev/null and b/docs/source/_static/actuators/armature-curve-dark.png differ diff --git a/docs/source/_static/actuators/armature-curve-light.png b/docs/source/_static/actuators/armature-curve-light.png new file mode 100644 index 000000000000..1a88de06f3f7 Binary files /dev/null and b/docs/source/_static/actuators/armature-curve-light.png differ diff --git a/docs/source/_static/actuators/damping-clip.webp b/docs/source/_static/actuators/damping-clip.webp new file mode 100644 index 000000000000..59d0bf23ada0 --- /dev/null +++ b/docs/source/_static/actuators/damping-clip.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e920ee2c0c41309560f3fe6a0addf0cd0f2f714c53f3b9be1a358406da525801 +size 610916 diff --git a/docs/source/_static/actuators/damping-curve-dark.png b/docs/source/_static/actuators/damping-curve-dark.png new file mode 100644 index 000000000000..7233195be122 Binary files /dev/null and b/docs/source/_static/actuators/damping-curve-dark.png differ diff --git a/docs/source/_static/actuators/damping-curve-light.png b/docs/source/_static/actuators/damping-curve-light.png new file mode 100644 index 000000000000..6cc9a44eebee Binary files /dev/null and b/docs/source/_static/actuators/damping-curve-light.png differ diff --git a/docs/source/_static/actuators/delay-clip.webp b/docs/source/_static/actuators/delay-clip.webp new file mode 100644 index 000000000000..a2a91ec44476 --- /dev/null +++ b/docs/source/_static/actuators/delay-clip.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1027f7a08dc392cda3106fc190820388e7c26a39bd3defb106f9ead39d2ebc7a +size 569220 diff --git a/docs/source/_static/actuators/delay-curve-dark.png b/docs/source/_static/actuators/delay-curve-dark.png new file mode 100644 index 000000000000..855b7a83d5fe Binary files /dev/null and b/docs/source/_static/actuators/delay-curve-dark.png differ diff --git a/docs/source/_static/actuators/delay-curve-light.png b/docs/source/_static/actuators/delay-curve-light.png new file mode 100644 index 000000000000..cd714e7443dc Binary files /dev/null and b/docs/source/_static/actuators/delay-curve-light.png differ diff --git a/docs/source/_static/actuators/effort-limit-clip.webp b/docs/source/_static/actuators/effort-limit-clip.webp new file mode 100644 index 000000000000..c64193b0572a --- /dev/null +++ b/docs/source/_static/actuators/effort-limit-clip.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b5bbae849f4fc69c2d6388845ee31b684539a85f66da13aa12791df1ead07ec +size 581568 diff --git a/docs/source/_static/actuators/effort-limit-curve-dark.png b/docs/source/_static/actuators/effort-limit-curve-dark.png new file mode 100644 index 000000000000..3ca91e3ab217 Binary files /dev/null and b/docs/source/_static/actuators/effort-limit-curve-dark.png differ diff --git a/docs/source/_static/actuators/effort-limit-curve-light.png b/docs/source/_static/actuators/effort-limit-curve-light.png new file mode 100644 index 000000000000..22b8c8c78af8 Binary files /dev/null and b/docs/source/_static/actuators/effort-limit-curve-light.png differ diff --git a/docs/source/_static/actuators/friction-clip.webp b/docs/source/_static/actuators/friction-clip.webp new file mode 100644 index 000000000000..bb5988d74b47 --- /dev/null +++ b/docs/source/_static/actuators/friction-clip.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:725cbb4170943629b9f49aa4373e61d12a6005d487dad8fd6c90ba95e89672b4 +size 1174082 diff --git a/docs/source/_static/actuators/friction-curve-dark.png b/docs/source/_static/actuators/friction-curve-dark.png new file mode 100644 index 000000000000..5860ca465990 Binary files /dev/null and b/docs/source/_static/actuators/friction-curve-dark.png differ diff --git a/docs/source/_static/actuators/friction-curve-light.png b/docs/source/_static/actuators/friction-curve-light.png new file mode 100644 index 000000000000..99b3fa675e47 Binary files /dev/null and b/docs/source/_static/actuators/friction-curve-light.png differ diff --git a/docs/source/_static/actuators/implicit-vs-explicit-curve-dark.png b/docs/source/_static/actuators/implicit-vs-explicit-curve-dark.png new file mode 100644 index 000000000000..e535b71b7f90 Binary files /dev/null and b/docs/source/_static/actuators/implicit-vs-explicit-curve-dark.png differ diff --git a/docs/source/_static/actuators/implicit-vs-explicit-curve-light.png b/docs/source/_static/actuators/implicit-vs-explicit-curve-light.png new file mode 100644 index 000000000000..2c6b3eb3a55c Binary files /dev/null and b/docs/source/_static/actuators/implicit-vs-explicit-curve-light.png differ diff --git a/docs/source/_static/actuators/pipeline-dark.png b/docs/source/_static/actuators/pipeline-dark.png new file mode 100644 index 000000000000..83eaded5c46a Binary files /dev/null and b/docs/source/_static/actuators/pipeline-dark.png differ diff --git a/docs/source/_static/actuators/pipeline-light.png b/docs/source/_static/actuators/pipeline-light.png new file mode 100644 index 000000000000..caf03a5f87cc Binary files /dev/null and b/docs/source/_static/actuators/pipeline-light.png differ diff --git a/docs/source/_static/actuators/stiffness-clip.webp b/docs/source/_static/actuators/stiffness-clip.webp new file mode 100644 index 000000000000..1ad8574e5107 --- /dev/null +++ b/docs/source/_static/actuators/stiffness-clip.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:393a7a6a28976a281cba02bc8bc98e6afbc1f74012fab81117fe7ac6b4fce18f +size 512942 diff --git a/docs/source/_static/actuators/stiffness-curve-dark.png b/docs/source/_static/actuators/stiffness-curve-dark.png new file mode 100644 index 000000000000..6bd5dd112d92 Binary files /dev/null and b/docs/source/_static/actuators/stiffness-curve-dark.png differ diff --git a/docs/source/_static/actuators/stiffness-curve-light.png b/docs/source/_static/actuators/stiffness-curve-light.png new file mode 100644 index 000000000000..4d73ab32dc74 Binary files /dev/null and b/docs/source/_static/actuators/stiffness-curve-light.png differ diff --git a/docs/source/_static/actuators/velocity-limit-curve-dark.png b/docs/source/_static/actuators/velocity-limit-curve-dark.png new file mode 100644 index 000000000000..948685be7a10 Binary files /dev/null and b/docs/source/_static/actuators/velocity-limit-curve-dark.png differ diff --git a/docs/source/_static/actuators/velocity-limit-curve-light.png b/docs/source/_static/actuators/velocity-limit-curve-light.png new file mode 100644 index 000000000000..72f5bc11d907 Binary files /dev/null and b/docs/source/_static/actuators/velocity-limit-curve-light.png differ diff --git a/docs/source/api/lab/isaaclab.actuators.rst b/docs/source/api/lab/isaaclab.actuators.rst index 5ab005de5b3b..d21e08907040 100644 --- a/docs/source/api/lab/isaaclab.actuators.rst +++ b/docs/source/api/lab/isaaclab.actuators.rst @@ -9,6 +9,10 @@ ActuatorBase ActuatorBaseCfg + ActuatorTargetCommand + ActuatorCollection + ActuatorControl + ActuatorOutputCommand ImplicitActuator ImplicitActuatorCfg IdealPDActuator @@ -24,6 +28,12 @@ ActuatorNetLSTM ActuatorNetLSTMCfg + .. rubric:: Functions + + .. autosummary:: + + resolve_joint_parameter + Actuator Base ------------- @@ -31,11 +41,33 @@ Actuator Base :members: :inherited-members: +.. autofunction:: resolve_joint_parameter + .. autoclass:: ActuatorBaseCfg :members: :inherited-members: :exclude-members: __init__, class_type +Actuator Collection +------------------- + +.. autoclass:: ActuatorCollection + :members: + :inherited-members: + +.. autoclass:: ActuatorTargetCommand + :members: + +.. autoclass:: ActuatorOutputCommand + :members: + +Actuator Control +---------------- + +.. autoclass:: ActuatorControl + :members: + :inherited-members: + Implicit Actuator ----------------- @@ -133,3 +165,19 @@ LSTM Network Actuator :inherited-members: :show-inheritance: :exclude-members: __init__, class_type + +Newton Actuator Access +---------------------- + +.. automodule:: isaaclab.actuators.newton + + .. rubric:: Functions + + .. autosummary:: + + read_group_parameter + write_group_parameter + +.. autofunction:: isaaclab.actuators.newton.read_group_parameter + +.. autofunction:: isaaclab.actuators.newton.write_group_parameter diff --git a/docs/source/concepts/actuators.rst b/docs/source/concepts/actuators.rst new file mode 100644 index 000000000000..fb8ddfd7f186 --- /dev/null +++ b/docs/source/concepts/actuators.rst @@ -0,0 +1,758 @@ +.. _overview-actuators: + + +Actuators +========= + +An articulated system moves through its actuated joints. Physical joints may use active components, +such as electric or hydraulic motors, or passive components, such as springs and friction. These +components can introduce finite torque, speed limits, delays, and gearbox effects. + +Isaac Lab exposes two ways to reproduce that behavior in simulation: + +* **Implicit actuators** pass position and velocity gains to the physics engine, which runs a + spring-damper (PD) controller in its solver. This is a low-overhead option when the built-in PD + model is sufficient. In isolation, PhysX implicit drives may tolerate higher gains or a larger + time step. Contacts, mimic joints, joint limits, large target changes, and insufficient solver + convergence can still make them unstable. See the `PhysX articulation drive stability guidance + `_. +* **Explicit actuators** run a model that computes joint effort, clips it to the motor's + capabilities, and submits the result. They can model saturation, delay, gearing, and learned motor + behavior. Native paths process commands differently: Newton processes them in the solver, while + PhysX and OVPhysX use a shared host adapter during ``write_data_to_sim()``. + +Actuator groups are configured through :attr:`~isaaclab.assets.ArticulationCfg.actuators` and +exposed at runtime through :class:`~isaaclab.actuators.ActuatorCollection` on +:attr:`~isaaclab.assets.Articulation.actuators`. The collection routes groups and stages commands +and telemetry. The articulation executes and submits them. LEAPP action terms still use the +articulation setters; see :ref:`actuators-migrating-setters`. + +.. contents:: On this page + :local: + :depth: 1 + + +Quick usage +----------- + +Declare one or more actuator groups in the articulation config. Each group selects a disjoint set +of joints by regular expression and chooses a model: + +.. code-block:: python + + from isaaclab.actuators import ImplicitActuatorCfg + from isaaclab.assets import ArticulationCfg + + robot_cfg = ArticulationCfg( + spawn=..., # your USD / spawner config + actuators={ + "legs": ImplicitActuatorCfg( + joint_names_expr=[".*_hip_.*", ".*_knee_.*"], + stiffness=40.0, + damping=2.0, + joint_effort_limit=80.0, + ), + }, + ) + +At runtime, send commands through :attr:`~isaaclab.actuators.ActuatorCollection.target_command`. Position +and velocity commands are expressed in joint-side coordinates, and every command buffer is indexed +by articulation joint. The setters are keyword-only and default to all environments and all joints: + +.. code-block:: python + + import torch + + # desired position for every joint of every environment + values = torch.full((robot.num_instances, robot.num_joints), 0.5, device=robot.device) + robot.actuators.target_command.set_position_index(value=values) + +The articulation stages and submits actuator commands inside +:meth:`~isaaclab.assets.Articulation.write_data_to_sim`. Isaac Lab-managed models and the shared +host adapter run during this call. Newton-native controllers finish processing inside the solver. +The following sections describe this pipeline, the available models, and their parameters. + + +.. _actuators-pipeline: + +The actuator pipeline +--------------------- + +Setting an actuator command does not write directly to the solver. Commands first enter the +collection. Each group then follows one of three execution paths: + +#. **Command view** -- ``actuators.target_command.set_*_index`` and ``_mask`` write desired position, + velocity, and effort into joint-indexed buffers. +#. **ActuatorCollection** -- routes groups and stages full-articulation commands, processed joint + commands, and telemetry. Actuator models retain their own gains and temporary state. +#. **Execution path** -- an Isaac Lab explicit model computes and clips effort, an implicit drive + applies targets with solver-side PD gains, or a native actuator runs through Newton or the shared + host adapter. + +For Isaac Lab-managed models, ``actuators.output_command`` contains the processed position, +velocity, and effort submitted to the backend. Native paths bypass this view, so it is not +submitted-command telemetry for them. PhysX and OVPhysX process commands during +``write_data_to_sim()``, while Newton-native controllers process them inside the solver. See +:ref:`actuators-native` for supported native models and limitations. + +.. figure:: ../_static/actuators/pipeline-light.png + :class: only-light + :align: center + :width: 90% + :alt: Three actuator paths: Lab-managed models execute before submission, implicit drives + execute in the solver, and native models execute in the Newton/MJWarp solver or in the + shared PhysX/OVPhysX host adapter before submission. + +.. figure:: ../_static/actuators/pipeline-dark.png + :class: only-dark + :align: center + :width: 90% + :alt: Three actuator paths: Lab-managed models execute before submission, implicit drives + execute in the solver, and native models execute in the Newton/MJWarp solver or in the + shared PhysX/OVPhysX host adapter before submission. + +Implicit and explicit groups store gains differently: + +* For an **implicit** group, :attr:`~isaaclab.actuators.ActuatorBaseCfg.stiffness` and + :attr:`~isaaclab.actuators.ActuatorBaseCfg.damping` are written to the solver, which + runs the PD law. ``compute()`` passes the desired targets through unchanged while recording an + approximate torque for telemetry. +* For an **explicit** group, the same gains are consumed by the model to compute a torque, and the + solver's own PD gains for those joints are set to zero. Reading ``data.joint_stiffness`` or + ``data.joint_damping`` on an explicit joint therefore returns **zero** -- the gains live in the + actuator model, not the solver. + +Gains configured for an actuator group are separate from +:attr:`~isaaclab.assets.ArticulationData.joint_stiffness` and +:attr:`~isaaclab.assets.ArticulationData.joint_damping`. Those data fields report solver drive +gains. They match implicit gains after initialization and are zero for explicit actuator joints. +They do not mirror actuator-model gains. + + +.. _actuators-joint-property-ownership: + +Joint and actuator property ownership +-------------------------------------- + +An actuator configuration selects joints and can override their properties during construction. +Isaac Lab resolves ``joint_effort_limit``, ``joint_velocity_limit``, armature, friction, and +implicit drive gains when it builds the articulation. +:class:`~isaaclab.assets.ArticulationData` holds the live values, including +``joint_effort_limits``, ``joint_vel_limits``, ``joint_armature``, and supported friction +properties. Use articulation data and joint writers to read or change them. +:class:`~isaaclab.actuators.ActuatorCollection` has no joint-property API. + +The runtime group properties ``effort_limit_sim``, ``velocity_limit_sim``, ``armature``, +``friction``, ``dynamic_friction``, and ``viscous_friction`` were removed. Read their live values +from articulation data and use the corresponding joint writers. The deprecated +``effort_limit_sim`` and ``velocity_limit_sim`` configuration aliases remain available through 3.x. +See :ref:`actuators-solver-limit-migration` for replacement data views and writers. + +Explicit actuators store model state such as ``actuator_effort_limit``, rated ``actuator_velocity_limit``, gains, +delay, and motor curves. They do not store separate solver-limit or friction values. Because the +backend runs their drives, implicit actuators read stiffness, damping, and effort projection from +live articulation properties; assigning to these properties is ignored with a warning — use the +articulation joint writers or the ``randomize_actuator_gains`` event instead. + +Newton-executed groups have no Isaac Lab model at all: the collection mapping entry is the owning +Newton ``Actuator`` object, whose controller keeps its parameters separate from solver gains. +Read or modify its components (``controller``, ``delay``, ``clamping``) directly for raw access, +or use :func:`~isaaclab.actuators.newton.read_group_parameter` and +:func:`~isaaclab.actuators.newton.write_group_parameter` for group-scoped access in +public joint order with environment selection. + +``actuator_effort_limit`` and ``actuator_velocity_limit`` apply to the actuator model. +``joint_effort_limit`` and ``joint_velocity_limit`` apply to the joint or solver and can have +different values. Explicit models clip output to ``actuator_effort_limit``; +``joint_effort_limit`` limits the solver. +``actuator_velocity_limit`` is the model's rated joint-side speed, or a soft-limit snapshot for +implicit actuators. ``joint_velocity_limit`` requests a solver constraint. Because backends +enforce it differently, solver velocity limits are not portable clamps. ``effort_limit`` and +``velocity_limit`` are deprecated configuration and runtime group aliases. ``velocity_limit`` +resolves to ``actuator_velocity_limit``; ``effort_limit`` resolves to ``actuator_effort_limit``. +An implicit group may configure ``actuator_effort_limit`` separately from ``joint_effort_limit`` +to keep a rated model-facing limit distinct from the solver clamp; when unset, it tracks the +live solver limit. + +Choosing a model +----------------- + +All models use :class:`~isaaclab.actuators.ActuatorBaseCfg`. PD models differ in clipping and +state. Neural models replace the analytical PD law with a learned torque predictor. Choose the +simplest model that meets your requirements. + +.. list-table:: + :header-rows: 1 + :widths: 22 34 24 20 + + * - Model (config) + - Torque / clipping + - Where limits clip + - Extra config fields + * - :class:`~isaaclab.actuators.ImplicitActuator` + (:class:`~isaaclab.actuators.ImplicitActuatorCfg`) + - Solver runs the PD law from the written gains. + - ``joint_effort_limit`` clips in the solver. + - -- + * - :class:`~isaaclab.actuators.IdealPDActuator` + (:class:`~isaaclab.actuators.IdealPDActuatorCfg`) + - :math:`\tau = k_p (q_{des}-q) + k_d(\dot{q}_{des}-\dot{q}) + \tau_{ff}` + - Model clips directly to :math:`\pm\,\tau_{max}` (``actuator_effort_limit``). + - -- + * - :class:`~isaaclab.actuators.DCMotor` + (:class:`~isaaclab.actuators.DCMotorCfg`) + - Same PD torque, clipped to a four-quadrant torque-speed envelope. + - Model clips against a velocity-dependent limit. + - ``saturation_effort``, ``actuator_velocity_limit`` + * - :class:`~isaaclab.actuators.DelayedPDActuator` + (:class:`~isaaclab.actuators.DelayedPDActuatorCfg`) + - Ideal PD applied to commands delayed by a circular buffer. + - Same as ideal PD (``actuator_effort_limit``). + - ``min_delay``, ``max_delay`` + * - :class:`~isaaclab.actuators.RemotizedPDActuator` + (:class:`~isaaclab.actuators.RemotizedPDActuatorCfg`) + - Delayed PD with an angle-dependent torque ceiling. + - Torque clipped by a joint-angle lookup table. + - ``joint_parameter_lookup`` + * - :class:`~isaaclab.actuators.ActuatorNetMLP` / + :class:`~isaaclab.actuators.ActuatorNetLSTM` + - A trained network predicts the torque from the joint history. + - Network output clipped by the DC-motor envelope. + - ``network_file`` (+ input scaling) + +**ImplicitActuator.** The default model. The solver applies the gains and limits. Isaac Lab +estimates effort telemetry from the current state when the backend does not expose it. + +**IdealPDActuator.** An explicit PD controller with feed-forward effort and a symmetric model-side +torque limit at :math:`\pm\,\tau_{max}`. + +**DCMotor.** Adds a linear four-quadrant torque-speed curve. ``saturation_effort`` is the stall +torque, and ``actuator_velocity_limit`` is the no-load speed. + +**DelayedPDActuator.** An ideal PD controller with delayed position, velocity, and effort commands. +The delay is sampled uniformly from ``[min_delay, max_delay]`` at reset. + +**RemotizedPDActuator.** A delayed PD controller with an angle-dependent torque limit. The +``joint_parameter_lookup`` table stores joint angle, transmission ratio, and maximum torque. Use it +for linkages whose effective lever arm changes through their range of motion. + +**ActuatorNetMLP / ActuatorNetLSTM.** Learned torque models that use joint-position error and +velocity history and clip output with the DC-motor envelope. They require a TorchScript checkpoint. +See the :mod:`isaaclab.actuators` API reference for configuration details. + + +.. _actuators-parameter-reference: + +Parameter reference +------------------- + +Each subsection compares five actuators while varying one parameter. All clips use a single-joint +pendulum stepped at :math:`dt = 1/360\text{ s}`, with commands issued at 60 Hz. The stiffness, +damping, and armature sweeps use the *implicit* path. + +.. important:: + + **Explicit groups keep the solver effort limit.** The solver retains the authored + :attr:`~isaaclab.actuators.ActuatorBaseCfg.joint_effort_limit` for explicit groups, so effort + submitted by an explicit model is clipped a second time by the solver. Configure + ``joint_effort_limit`` at least as large as ``actuator_effort_limit`` when the model should be + the only clip. See :ref:`actuators-joint-property-ownership` for model-limit, joint-limit, and + implicit-alias semantics. + + +Stiffness +^^^^^^^^^ + +Stiffness (:math:`k_p`, the proportional gain) controls how strongly the joint moves toward its +position target. Higher stiffness improves tracking but can increase overshoot and oscillation. +Too little stiffness leaves steady-state error under load. Tune stiffness together with damping. +Units are [N·m/rad] for revolute joints and [N/m] for prismatic joints. + +.. figure:: ../_static/actuators/stiffness-clip.webp + :align: center + :width: 100% + :alt: Five pendulums with increasing stiffness stepping to the same target. + +.. figure:: ../_static/actuators/stiffness-curve-light.png + :class: only-light + :align: center + :width: 80% + :alt: Position step response for a stiffness sweep. + +.. figure:: ../_static/actuators/stiffness-curve-dark.png + :class: only-dark + :align: center + :width: 80% + :alt: Position step response for a stiffness sweep. + + +Damping +^^^^^^^ + +Damping (:math:`k_d`, the derivative gain) resists joint velocity. Too little damping allows a +stiff joint to oscillate. More damping reduces overshoot until the joint becomes critically damped; +beyond that point, the response becomes sluggish. Damping also sets the tracking gain for velocity +targets. Units are [N·m·s/rad] for revolute joints and [N·s/m] for prismatic joints. + +.. figure:: ../_static/actuators/damping-clip.webp + :align: center + :width: 100% + :alt: Five pendulums from underdamped to overdamped stepping to the same target. + +.. figure:: ../_static/actuators/damping-curve-light.png + :class: only-light + :align: center + :width: 80% + :alt: Position step response for a damping sweep. + +.. figure:: ../_static/actuators/damping-curve-dark.png + :class: only-dark + :align: center + :width: 80% + :alt: Position step response for a damping sweep. + + +Armature +^^^^^^^^ + +Armature [kg or kg·m², depending on joint type] represents the reflected inertia of the motor and +gearbox. It is added to the joint-space inertia, so it changes the physical model. In this +single-joint comparison, more armature slows the response and increases the stability margin for +the selected gains. It can also improve numerical conditioning for low-inertia joints. + +Explicit and implicit actuators may require different stability settings because explicit models +submit effort while implicit models use solver drives. If an explicit simulation diverges, check +mass and inertia, target scaling and update rate, effort limits, gains, time step, and solver +convergence. Choose armature from the motor and transmission model. See the `OmniPhysics articulation stability guide +`_ +for more information. + +.. figure:: ../_static/actuators/armature-clip.webp + :align: center + :width: 100% + :alt: Five pendulums with increasing armature responding to the same command. + +.. figure:: ../_static/actuators/armature-curve-light.png + :class: only-light + :align: center + :width: 80% + :alt: Position step response for an armature sweep. + +.. figure:: ../_static/actuators/armature-curve-dark.png + :class: only-dark + :align: center + :width: 80% + :alt: Position step response for an armature sweep. + + +Friction +^^^^^^^^ + +Joint friction acts independently of the PD command. On a freely spinning joint, higher friction +removes velocity faster. Isaac Lab supports static +(:attr:`~isaaclab.actuators.ActuatorBaseCfg.friction`), dynamic +(:attr:`~isaaclab.actuators.ActuatorBaseCfg.dynamic_friction`), and viscous +(:attr:`~isaaclab.actuators.ActuatorBaseCfg.viscous_friction`) friction. Use it to model gearbox +stiction and drag, not to stabilize a controller. + +.. note:: + + Friction conventions depend on the backend. PhysX uses dimensionless static and dynamic + coefficients in Isaac Sim 4.5, and effort values [N or N·m, depending on joint type] in 5.0 + and later. OVPhysX uses dimensionless static and dynamic coefficients. Newton uses a dry-friction + effort and has no separate dynamic-friction value. All three use viscous damping + [N·s/m or N·m·s/rad, depending on joint type]. + +.. figure:: ../_static/actuators/friction-clip.webp + :align: center + :width: 100% + :alt: Five free-spinning pendulums with increasing joint friction decaying at different rates. + +.. figure:: ../_static/actuators/friction-curve-light.png + :class: only-light + :align: center + :width: 80% + :alt: Joint-velocity decay for a friction sweep. + +.. figure:: ../_static/actuators/friction-curve-dark.png + :class: only-dark + :align: center + :width: 80% + :alt: Joint-velocity decay for a friction sweep. + + +Effort limit +^^^^^^^^^^^^ + +The effort limit is the torque ceiling the motor can produce [N·m or N]. The clip below drives an +:class:`~isaaclab.actuators.IdealPDActuator` swing-up from hanging to horizontal against a +~2.94 N·m gravity-hold torque, with the model's ``actuator_effort_limit`` swept over +:math:`[1, 2, 3, 4, 6]` N·m. + +The limit shapes the transient response, while gravity determines the steady state. Limits above +the approximately 2.94 N·m hold torque reach the horizontal target. Lower limits leave the joint +below it, where gravity torque (:math:`\approx 2.94 \sin\theta`) matches the limit. When the PD +demand exceeds the limit, the applied torque and damping term are both clipped. The joint can +therefore oscillate until the demand returns within the limit. An effort limit below the load's +static demand prevents the controller from damping the joint effectively. + +.. figure:: ../_static/actuators/effort-limit-clip.webp + :align: center + :width: 100% + :alt: Five pendulums with increasing effort limit holding or failing against gravity. + +.. figure:: ../_static/actuators/effort-limit-curve-light.png + :class: only-light + :align: center + :width: 80% + :alt: Applied joint torque for an effort-limit sweep. + +.. figure:: ../_static/actuators/effort-limit-curve-dark.png + :class: only-dark + :align: center + :width: 80% + :alt: Applied joint torque for an effort-limit sweep. + + +Velocity limit +^^^^^^^^^^^^^^ + +For a :class:`~isaaclab.actuators.DCMotor`, ``actuator_velocity_limit`` is the no-load speed [rad/s or m/s]. +Torque decreases as the joint approaches this speed, forming the four-quadrant torque-speed +envelope. Lower limits reduce the usable speed range and cause earlier clipping. For an implicit +group, the value is exposed through the soft velocity-limit view. ``joint_velocity_limit`` instead +requests a solver constraint. Enforcement depends on the backend; see +:ref:`newton-velocity-limits`. + +.. figure:: ../_static/actuators/velocity-limit-curve-light.png + :class: only-light + :align: center + :width: 80% + :alt: Torque-speed envelope for a velocity-limit sweep. + +.. figure:: ../_static/actuators/velocity-limit-curve-dark.png + :class: only-dark + :align: center + :width: 80% + :alt: Torque-speed envelope for a velocity-limit sweep. + + +Command delay +^^^^^^^^^^^^^ + +A :class:`~isaaclab.actuators.DelayedPDActuator` delays every command by a fixed number of physics +steps sampled from ``[min_delay, max_delay]`` at reset. The clip compares delays of +:math:`[0, 6, 12, 24, 48]` steps (0--133 ms at :math:`dt = 1/360\text{ s}`) for a square-wave +position command. Longer delays make the pendulum trail the reference. Randomizing delay between +resets can model transport delay during sim-to-real training. + +.. figure:: ../_static/actuators/delay-clip.webp + :align: center + :width: 100% + :alt: Five pendulums with increasing command delay trailing the same square-wave command. + +.. figure:: ../_static/actuators/delay-curve-light.png + :class: only-light + :align: center + :width: 80% + :alt: Command-versus-response timeline for a delay sweep. + +.. figure:: ../_static/actuators/delay-curve-dark.png + :class: only-dark + :align: center + :width: 80% + :alt: Command-versus-response timeline for a delay sweep. + + +Implicit vs. explicit +^^^^^^^^^^^^^^^^^^^^^ + +In this single-joint demo at :math:`dt = 1/360\text{ s}`, implicit and ideal-PD explicit actuators +with identical gains produce nearly the same response. Outside this example, their response and +stability can differ. The solver applies the implicit drive, whereas the explicit model evaluates +PD once per step. Policies trained with implicit actuators may therefore need adjustment for +explicit actuators. For explicit joints, ``data.joint_stiffness`` and ``data.joint_damping`` are +zero because the gains belong to the actuator model. + +.. figure:: ../_static/actuators/implicit-vs-explicit-curve-light.png + :class: only-light + :align: center + :width: 80% + :alt: Overlaid implicit and explicit PD step responses at identical gains. + +.. figure:: ../_static/actuators/implicit-vs-explicit-curve-dark.png + :class: only-dark + :align: center + :width: 80% + :alt: Overlaid implicit and explicit PD step responses at identical gains. + + +.. _actuators-runtime-api: + +Runtime API: ``articulation.actuators`` +--------------------------------------- + +Group access +^^^^^^^^^^^^ + +:attr:`~isaaclab.assets.Articulation.actuators` is an +:class:`~isaaclab.actuators.ActuatorCollection`, a read-only ``Mapping`` from group name to +whoever owns the group. Isaac Lab-executed groups map to their +:class:`~isaaclab.actuators.ActuatorBase` model instances. Newton-executed groups map to the +Newton ``Actuator`` objects that drive their joints — no Isaac Lab model exists for them, so +their controller parameters are read and modified on the owning object (see +:ref:`actuators-native`). Membership is fixed after construction, so you can look up and iterate +groups but not add, replace, or delete them: + +.. code-block:: python + + legs = robot.actuators["legs"] # the group's owner: an ActuatorBase, or a + # Newton Actuator under use_newton_actuators=True + for name, actuator in robot.actuators.items(): + print(name, type(actuator).__name__) + +Configure topology before creating the articulation: + +.. code-block:: python + + robot_cfg.actuators["gripper"] = ImplicitActuatorCfg(...) + robot = Articulation(robot_cfg) + +At runtime, both ``robot.actuators["gripper"] = ...`` and +``del robot.actuators["gripper"]`` raise :class:`TypeError`. Group membership and joint coverage +are fixed when the articulation is created. + +Logical groups and execution batches +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Named groups such as ``hips`` and ``knees`` remain distinct when you configure or access them. +A single internal executor computes all plain implicit groups in one fused kernel launch without +changing the tensors returned by ``robot.actuators["hips"]``. Each joint may belong to only one +group; overlapping selections raise :class:`ValueError`. Explicit, stateful, neural, and +subclassed groups execute one group at a time on the Isaac Lab path, and Newton-executed groups +run inside the solver or host adapter. Fused execution is an internal optimization. + +Commands, telemetry, and lifecycle +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Setting actuator commands.** The mutable ``target_command`` view contains the desired position, +velocity, and effort. Use index setters with environment or joint index lists and tensors. Use mask +setters when you already have boolean Warp masks. All setters are keyword-only: + +.. code-block:: python + + import torch + + ids = torch.tensor([0, 1], device=robot.device) # first two joints + env_ids = torch.arange(robot.num_instances, device=robot.device) + sub = torch.zeros((env_ids.numel(), ids.numel()), device=robot.device) + robot.actuators.target_command.set_position_index(value=sub, joint_ids=ids, env_ids=env_ids) + +``env_ids`` and ``joint_ids`` must not contain duplicates. Duplicate indices write to the same +destination concurrently, so the result is undefined. Remove duplicates or use a boolean mask, +which selects each destination at most once. + +By default, ``value`` has shape ``(len(env_ids), len(joint_ids))``. Pass ``full_data=True`` when +``value`` already has shape ``(num_instances, num_joints)``. The setter then reads the selected +values directly from the full buffer. + +**Reading commands and telemetry.** ``target_command`` contains the desired values staged for +actuator processing. For Isaac Lab-managed models, ``output_command`` contains the processed joint +commands. +Native paths bypass this view, so it does not show submitted commands. Newton processes commands in +the solver, while PhysX and OVPhysX process them through the shared host adapter during +``write_data_to_sim()``. Access the arrays through ``.torch`` or ``.warp``: + +.. code-block:: python + + desired_position = robot.actuators.target_command.position.torch + processed_effort = robot.actuators.output_command.effort.torch # Isaac Lab-managed path + applied = robot.actuators.applied_effort.torch # after clipping [N·m or N] + computed = robot.actuators.computed_effort.torch # before clipping [N·m or N] + +:attr:`~isaaclab.actuators.ActuatorCollection.computed_effort` is the model output before clipping. +:attr:`~isaaclab.actuators.ActuatorCollection.applied_effort` is the value after clipping. For +implicit actuators, these are approximate efforts recorded for rewards and telemetry. + +**Randomizing gains.** Gain updates depend on the actuator implementation. Managed environments +should use the ``randomize_actuator_gains`` event. It updates actuator-owned gains, implicit solver +drives, or native-controller parameters as appropriate. There is no generic collection-level +stiffness or damping writer. + +**Lifecycle.** Do not call ``compute()`` or ``submit_commands()`` directly. The articulation resets +actuators during environment resets. It runs actuator compute, staging, and submission from +:meth:`~isaaclab.assets.Articulation.write_data_to_sim`. Call this method before advancing the +simulation, or let the scene or environment loop call it. The effort telemetry reflects its most +recent call. Newton-native processing continues inside the solver; host-adapter processing finishes +during the call. + +.. _actuators-migrating-setters: + +.. rubric:: Migrating from the deprecated setters + +Joint commands were previously set on the articulation. Those methods now forward to the collection +and emit a :class:`DeprecationWarning`: + +.. code-block:: python + + # Before (deprecated) + robot.set_joint_position_target(target, joint_ids=ids) + + # After + robot.actuators.target_command.set_position_index(value=target, joint_ids=ids) + +LEAPP-exported action terms must keep using the annotated articulation ``*_index`` or ``*_mask`` +setters until the exporter supports collection setters. Other runtime code should migrate to the +collection API. + +The data accessors also moved: ``articulation.data.joint_pos_target`` becomes +``robot.actuators.target_command.position``, and ``data.computed_torque`` / ``data.applied_torque`` become +``robot.actuators.computed_effort`` / ``robot.actuators.applied_effort``. See the +:doc:`Isaac Lab 3.0 migration guide <../migration/migrating_to_isaaclab_3-0>` for the full table. + + +Value resolution: USD vs. ActuatorCfg +------------------------------------- + +Gains and limits can come from the USD joint-drive prim or the actuator config. If a config field is +``None``, Isaac Lab uses the USD value. A value set in the config overrides the USD value. The robot +schema supplies joint and body ordering, not actuator parameters. + +Set :attr:`~isaaclab.assets.ArticulationCfg.actuator_value_resolution_debug_print` to ``True`` to +log the USD, configuration, and applied values for every joint when the sources differ or the +configuration leaves a value unspecified. Unspecified values appear as ``Not Specified``. See +:ref:`how-to-write-articulation-config` for examples and the limit table. + + +.. _actuators-native: + +Native actuators +---------------- + +By default, Isaac Lab runs explicit actuator models once per step outside the solver, usually on +Torch or Warp. This path is deprecated. Set +:attr:`~isaaclab.sim.SimulationCfg.use_newton_actuators` to ``True`` to use the native path for +supported explicit models: + +.. code-block:: python + + from isaaclab.sim import SimulationCfg + + sim_cfg = SimulationCfg(use_newton_actuators=True) + +With the flag enabled, each supported explicit actuator config becomes a ``NewtonActuator`` USD +prim. Newton executes it in the solver. PhysX and OVPhysX execute the same model through the shared +host adapter during :meth:`~isaaclab.assets.Articulation.write_data_to_sim`. + +On Newton, native actuators run in the CUDA-graph-captured region. Implicit actuators are unchanged: +the solver still applies their PD gains. On CUDA, the host adapter captures actuator staging, model +execution, and telemetry publication when possible; otherwise it uses eager execution. Stateful +native actuators cannot run in a caller-owned CUDA graph, so the host adapter manages them. + +Newton executes native actuators in its controller, and the collection exposes that ownership +directly: ``robot.actuators[name]`` returns the Newton ``Actuator`` object driving the group's +joints instead of an Isaac Lab model. Newton merges structurally identical joints into one +actuator, so several groups can share an object (a group spanning several returns them as a +tuple). Raw component access reads and writes the controller storage in Newton's layout; for +group-scoped access in public joint order, use +:func:`~isaaclab.actuators.newton.read_group_parameter` and +:func:`~isaaclab.actuators.newton.write_group_parameter`: + +.. code-block:: python + + # raw ownership: the Newton actuator object itself + legs = robot.actuators["legs"] + print(type(legs.controller).__name__) + + # group-scoped, user-ordered parameter access + from isaaclab.actuators.newton import read_group_parameter, write_group_parameter + + kp = read_group_parameter(robot.actuators, "legs", "controller", "kp") + write_group_parameter(robot.actuators, "legs", "controller", "kp", values=kp * 2.0) + +Isaac Lab retains named groups for configuration, joint bookkeeping, and command and telemetry +staging. + +**Supported models.** Each supported config maps to USD schemas: + +.. list-table:: + :header-rows: 1 + :widths: 45 55 + + * - Config + - USD schemas + * - :class:`~isaaclab.actuators.IdealPDActuatorCfg` + - ``NewtonPDControlAPI`` + ``NewtonMaxEffortClampingAPI`` + * - :class:`~isaaclab.actuators.DCMotorCfg` + - ``NewtonPDControlAPI`` + ``NewtonDCMotorClampingAPI`` + * - :class:`~isaaclab.actuators.DelayedPDActuatorCfg` + - ideal PD + ``NewtonActuatorDelayAPI`` + * - :class:`~isaaclab.actuators.RemotizedPDActuatorCfg` + - delayed PD + ``NewtonPositionBasedClampingAPI`` + * - :class:`~isaaclab.actuators.ActuatorNetMLPCfg` / + :class:`~isaaclab.actuators.ActuatorNetLSTMCfg` + - ``NewtonNeuralControlAPI`` (+ ``NewtonDCMotorClampingAPI``) + +**Existing USD actuators.** For joints covered by an explicit Lab actuator config, the config +replaces any existing ``NewtonActuator`` prim. Joints not covered by a Lab config keep their +USD-authored actuators. USD-authored and Lab-configured actuators can therefore coexist on different +joints. + +.. warning:: + + With ``use_newton_actuators=True``, every explicit actuator config must be supported. An + unsupported config raises an error before native authoring. Disable ``use_newton_actuators`` to + use the Isaac Lab execution path, or select a supported config. + +.. note:: + + Under native execution, delay is fixed. The schema stores only ``max_delay``, so ``min_delay`` + is ignored. A :class:`~isaaclab.actuators.DelayedPDActuator` does not randomize delay between + resets as it does on the Isaac Lab path. + + +Backend submission +------------------ + +Submission differs by backend, but the collection interface is the same: + +.. list-table:: + :header-rows: 1 + :widths: 24 76 + + * - Backend + - Submit behavior + * - PhysX + - The Isaac Lab-managed path pushes processed position, velocity, and effort staging buffers + through the PhysX Tensor API. The shared host adapter processes native commands during + ``write_data_to_sim()`` and submits raw position and velocity targets plus raw effort. + ``applied_effort`` remains telemetry. A fused reorder gather runs first when a non-identity + joint ordering is active. + * - OVPhysX + - The native path uses the shared host adapter during ``write_data_to_sim()``. It writes raw + position and velocity targets plus raw effort through ``set_attribute`` tensor bindings. + ``applied_effort`` remains telemetry. Raw setter writes are mirrored into the binding + immediately. A fused reorder gather runs first when joint ordering is non-identity. + * - Newton / MJWarp + - Newton writes targets to solver-bound control arrays. Effort is feed-forward, and native + actuators also use ``joint_act``. The built-in drive applies PD to implicit joints. + +For explicit actuators, solver stiffness and damping are zero on every backend because the actuator +model owns the gains. + +This page does **not** cover: + +* Motion generators or low-level control modes -- see + :doc:`/source/overview/core-concepts/motion_generators`. +* Cross-backend policy transfer and solver-dynamics differences -- see + :doc:`/source/how-to/transfer_policies_between_physx_and_newton`. + +.. seealso:: + + * :ref:`how-to-write-articulation-config` -- authoring configs and the full gain/limit value + resolution walkthrough. + * :ref:`import-new-asset-ensure-drives-exist` -- making sure joint drives exist on an imported + asset so actuators can bind. + * ``Joints actuate in PhysX but not in a Newton-based backend`` in + :doc:`../refs/troubleshooting` -- the USD drive / backend actuation pitfall. + * :mod:`isaaclab.actuators` -- the full actuator model and config API reference. diff --git a/docs/source/how-to/robots.rst b/docs/source/how-to/robots.rst index 39b011156b52..5e3c33d8c020 100644 --- a/docs/source/how-to/robots.rst +++ b/docs/source/how-to/robots.rst @@ -38,22 +38,22 @@ the Dofbot actuators={ "front_joints": ImplicitActuatorCfg( joint_names_expr=["joint[1-2]"], - effort_limit_sim=100.0, - velocity_limit_sim=100.0, + joint_effort_limit=100.0, + joint_velocity_limit=100.0, stiffness=10000.0, damping=100.0, ), "joint3_act": ImplicitActuatorCfg( joint_names_expr=["joint3"], - effort_limit_sim=100.0, - velocity_limit_sim=100.0, + joint_effort_limit=100.0, + joint_velocity_limit=100.0, stiffness=10000.0, damping=100.0, ), "joint4_act": ImplicitActuatorCfg( joint_names_expr=["joint4"], - effort_limit_sim=100.0, - velocity_limit_sim=100.0, + joint_effort_limit=100.0, + joint_velocity_limit=100.0, stiffness=10000.0, damping=100.0, ), diff --git a/docs/source/how-to/transfer_policies_between_physx_and_newton.rst b/docs/source/how-to/transfer_policies_between_physx_and_newton.rst index 72be90b719b0..b05394c7ce61 100644 --- a/docs/source/how-to/transfer_policies_between_physx_and_newton.rst +++ b/docs/source/how-to/transfer_policies_between_physx_and_newton.rst @@ -123,11 +123,11 @@ Transferring control behavior Match the nominal actuator response before tuning the policy: -* distinguish physical ``velocity_limit`` from numerical ``velocity_limit_sim`` -* use per-joint effort, stiffness, damping, friction, and armature -* preserve ``dt * decimation`` and action hold -* keep targets away from hard joint stops -* monitor saturation and consecutive action sign changes +* distinguish the physical ``actuator_velocity_limit`` from the solver ``joint_velocity_limit``; +* use per-joint effort, stiffness, damping, friction, and armature; +* preserve ``dt * decimation`` and action hold; +* keep targets away from hard joint stops; +* monitor saturation and consecutive action sign changes. Increased damping is often necessary to prevent bang-bang control. With too little damping, a position policy can alternate saturated commands and exploit one solver's drive integration or diff --git a/docs/source/how-to/write_articulation_cfg.rst b/docs/source/how-to/write_articulation_cfg.rst index 40f74f873234..09ec76c74c50 100644 --- a/docs/source/how-to/write_articulation_cfg.rst +++ b/docs/source/how-to/write_articulation_cfg.rst @@ -109,8 +109,8 @@ to combine them into a single actuator model. actuators={ "all_joints": ImplicitActuatorCfg( joint_names_expr=[".*"], - effort_limit=400.0, - velocity_limit=100.0, + joint_effort_limit=400.0, + joint_velocity_limit=100.0, stiffness={"slider_to_cart": 0.0, "cart_to_pole": 0.0}, damping={"slider_to_cart": 10.0, "cart_to_pole": 0.0}, ), @@ -131,65 +131,39 @@ to combine them into a single actuator model. ActuatorCfg velocity/effort limits considerations ------------------------------------------------- -In IsaacLab v1.4.0, the plain ``velocity_limit`` and ``effort_limit`` attributes were **not** consistently -pushed into the physics solver: - -- **Implicit actuators** - - velocity_limit was ignored (never set in simulation) - - effort_limit was set into simulation - -- **Explicit actuators** - - both velocity_limit and effort_limit were used only by the drive model, not by the solver - - -In v2.0.1 we accidentally changed this: all velocity_limit & effort_limit, implicit or -explicit, were being applied to the solver. That caused many training under the old default uncaped solver -limits to break. - -To restore the original behavior while still giving users full control over solver limits, we introduced two new flags: - -* **velocity_limit_sim** - Sets the physics-solver's maximum joint-velocity cap in simulation. - -* **effort_limit_sim** - Sets the physics-solver's maximum joint-effort cap in simulation. - - -These explicitly set the solver's joint-velocity and joint-effort caps at simulation level. - -On the other hand, velocity_limit and effort_limit model the motor's hardware-level constraints in torque -computation for all explicit actuators rather than limiting simulation-level constraint. -For implicit actuators, since they do not model motor hardware limitations, ``velocity_limit`` were removed in v2.1.1 -and marked as deprecated. This preserves same behavior as they did in v1.4.0. Eventually, ``velocity_limit`` and -``effort_limit`` will be deprecated for implicit actuators, preserving only ``velocity_limit_sim`` and -``effort_limit_sim`` - - -.. table:: Limit Options Comparison - - .. list-table:: - :header-rows: 1 - :widths: 20 40 40 - - * - **Attribute** - - **Implicit Actuator** - - **Explicit Actuator** - * - ``velocity_limit`` - - Deprecated (alias for ``velocity_limit_sim``) - - Used by the model (e.g. DC motor), not set into simulation - * - ``effort_limit`` - - Deprecated (alias for ``effort_limit_sim``) - - Used by the model, not set into simulation - * - ``velocity_limit_sim`` - - Set into simulation - - Set into simulation - * - ``effort_limit_sim`` - - Set into simulation - - Set into simulation - - - -Users who want to tune the underlying physics-solver limits should set the ``_sim`` flags. +Use the following fields in an actuator configuration. They select joints and are resolved when the +articulation is constructed; the canonical runtime values live on +:class:`~isaaclab.assets.ArticulationData`. See :ref:`actuators-joint-property-ownership` for the +ownership model and runtime mutation paths. + +.. list-table:: Limit configuration + :header-rows: 1 + :widths: 28 36 36 + + * - Field + - Implicit actuator + - Explicit actuator + * - ``joint_effort_limit`` + - Writes the solver drive effort limit. + - Writes the solver effort limit; defaults high to avoid a second model clip. + * - ``actuator_effort_limit`` + - Not supported. + - Clips actuator-model output. + * - ``joint_velocity_limit`` + - Requests a solver velocity constraint. + - Requests a solver velocity constraint. + * - ``actuator_velocity_limit`` + - Creates the soft velocity-limit snapshot; it is not a solver request. + - Describes the actuator rated speed; speed-dependent models use it in their torque curve. + * - ``effort_limit`` + - Deprecated alias for ``joint_effort_limit``. + - Deprecated alias for ``actuator_effort_limit``. + * - ``velocity_limit`` + - Deprecated alias for ``actuator_velocity_limit``. + - Deprecated alias for ``actuator_velocity_limit``. + +Solver velocity enforcement is backend-dependent. ``joint_velocity_limit`` records the requested +joint state but is not a backend-independent safety clamp; see :ref:`newton-velocity-limits`. USD vs. ActuatorCfg discrepancy resolution @@ -231,49 +205,49 @@ if unmatching value is found, we log the resolution: This resolution info is emitted as a warning table only when discrepancies exist. Here's an example of what you'll see:: - +----------------+--------------------+---------------------+----+-------------+--------------------+----------+ - | Group | Property | Name | ID | USD Value | ActuatorCfg Value | Applied | - +----------------+--------------------+---------------------+----+-------------+--------------------+----------+ - | panda_shoulder | velocity_limit_sim | panda_joint1 | 0 | 2.17e+00 | Not Specified | 2.17e+00 | - | | | panda_joint2 | 1 | 2.17e+00 | Not Specified | 2.17e+00 | - | | | panda_joint3 | 2 | 2.17e+00 | Not Specified | 2.17e+00 | - | | | panda_joint4 | 3 | 2.17e+00 | Not Specified | 2.17e+00 | - | | stiffness | panda_joint1 | 0 | 2.29e+04 | 8.00e+01 | 8.00e+01 | - | | | panda_joint2 | 1 | 2.29e+04 | 8.00e+01 | 8.00e+01 | - | | | panda_joint3 | 2 | 2.29e+04 | 8.00e+01 | 8.00e+01 | - | | | panda_joint4 | 3 | 2.29e+04 | 8.00e+01 | 8.00e+01 | - | | damping | panda_joint1 | 0 | 4.58e+03 | 4.00e+00 | 4.00e+00 | - | | | panda_joint2 | 1 | 4.58e+03 | 4.00e+00 | 4.00e+00 | - | | | panda_joint3 | 2 | 4.58e+03 | 4.00e+00 | 4.00e+00 | - | | | panda_joint4 | 3 | 4.58e+03 | 4.00e+00 | 4.00e+00 | - | | armature | panda_joint1 | 0 | 0.00e+00 | Not Specified | 0.00e+00 | - | | | panda_joint2 | 1 | 0.00e+00 | Not Specified | 0.00e+00 | - | | | panda_joint3 | 2 | 0.00e+00 | Not Specified | 0.00e+00 | - | | | panda_joint4 | 3 | 0.00e+00 | Not Specified | 0.00e+00 | - | panda_forearm | velocity_limit_sim | panda_joint5 | 4 | 2.61e+00 | Not Specified | 2.61e+00 | - | | | panda_joint6 | 5 | 2.61e+00 | Not Specified | 2.61e+00 | - | | | panda_joint7 | 6 | 2.61e+00 | Not Specified | 2.61e+00 | - | | stiffness | panda_joint5 | 4 | 2.29e+04 | 8.00e+01 | 8.00e+01 | - | | | panda_joint6 | 5 | 2.29e+04 | 8.00e+01 | 8.00e+01 | - | | | panda_joint7 | 6 | 2.29e+04 | 8.00e+01 | 8.00e+01 | - | | damping | panda_joint5 | 4 | 4.58e+03 | 4.00e+00 | 4.00e+00 | - | | | panda_joint6 | 5 | 4.58e+03 | 4.00e+00 | 4.00e+00 | - | | | panda_joint7 | 6 | 4.58e+03 | 4.00e+00 | 4.00e+00 | - | | armature | panda_joint5 | 4 | 0.00e+00 | Not Specified | 0.00e+00 | - | | | panda_joint6 | 5 | 0.00e+00 | Not Specified | 0.00e+00 | - | | | panda_joint7 | 6 | 0.00e+00 | Not Specified | 0.00e+00 | - | | friction | panda_joint5 | 4 | 0.00e+00 | Not Specified | 0.00e+00 | - | | | panda_joint6 | 5 | 0.00e+00 | Not Specified | 0.00e+00 | - | | | panda_joint7 | 6 | 0.00e+00 | Not Specified | 0.00e+00 | - | panda_hand | velocity_limit_sim | panda_finger_joint1 | 7 | 2.00e-01 | Not Specified | 2.00e-01 | - | | | panda_finger_joint2 | 8 | 2.00e-01 | Not Specified | 2.00e-01 | - | | stiffness | panda_finger_joint1 | 7 | 1.00e+06 | 2.00e+03 | 2.00e+03 | - | | | panda_finger_joint2 | 8 | 1.00e+06 | 2.00e+03 | 2.00e+03 | - | | armature | panda_finger_joint1 | 7 | 0.00e+00 | Not Specified | 0.00e+00 | - | | | panda_finger_joint2 | 8 | 0.00e+00 | Not Specified | 0.00e+00 | - | | friction | panda_finger_joint1 | 7 | 0.00e+00 | Not Specified | 0.00e+00 | - | | | panda_finger_joint2 | 8 | 0.00e+00 | Not Specified | 0.00e+00 | - +----------------+--------------------+---------------------+----+-------------+--------------------+----------+ + +----------------+------------------------+---------------------+----+-------------+--------------------+----------+ + | Group | Property | Name | ID | USD Value | ActuatorCfg Value | Applied | + +----------------+------------------------+---------------------+----+-------------+--------------------+----------+ + | panda_shoulder | joint_velocity_limit | panda_joint1 | 0 | 2.17e+00 | Not Specified | 2.17e+00 | + | | | panda_joint2 | 1 | 2.17e+00 | Not Specified | 2.17e+00 | + | | | panda_joint3 | 2 | 2.17e+00 | Not Specified | 2.17e+00 | + | | | panda_joint4 | 3 | 2.17e+00 | Not Specified | 2.17e+00 | + | | stiffness | panda_joint1 | 0 | 2.29e+04 | 8.00e+01 | 8.00e+01 | + | | | panda_joint2 | 1 | 2.29e+04 | 8.00e+01 | 8.00e+01 | + | | | panda_joint3 | 2 | 2.29e+04 | 8.00e+01 | 8.00e+01 | + | | | panda_joint4 | 3 | 2.29e+04 | 8.00e+01 | 8.00e+01 | + | | damping | panda_joint1 | 0 | 4.58e+03 | 4.00e+00 | 4.00e+00 | + | | | panda_joint2 | 1 | 4.58e+03 | 4.00e+00 | 4.00e+00 | + | | | panda_joint3 | 2 | 4.58e+03 | 4.00e+00 | 4.00e+00 | + | | | panda_joint4 | 3 | 4.58e+03 | 4.00e+00 | 4.00e+00 | + | | armature | panda_joint1 | 0 | 0.00e+00 | Not Specified | 0.00e+00 | + | | | panda_joint2 | 1 | 0.00e+00 | Not Specified | 0.00e+00 | + | | | panda_joint3 | 2 | 0.00e+00 | Not Specified | 0.00e+00 | + | | | panda_joint4 | 3 | 0.00e+00 | Not Specified | 0.00e+00 | + | panda_forearm | joint_velocity_limit | panda_joint5 | 4 | 2.61e+00 | Not Specified | 2.61e+00 | + | | | panda_joint6 | 5 | 2.61e+00 | Not Specified | 2.61e+00 | + | | | panda_joint7 | 6 | 2.61e+00 | Not Specified | 2.61e+00 | + | | stiffness | panda_joint5 | 4 | 2.29e+04 | 8.00e+01 | 8.00e+01 | + | | | panda_joint6 | 5 | 2.29e+04 | 8.00e+01 | 8.00e+01 | + | | | panda_joint7 | 6 | 2.29e+04 | 8.00e+01 | 8.00e+01 | + | | damping | panda_joint5 | 4 | 4.58e+03 | 4.00e+00 | 4.00e+00 | + | | | panda_joint6 | 5 | 4.58e+03 | 4.00e+00 | 4.00e+00 | + | | | panda_joint7 | 6 | 4.58e+03 | 4.00e+00 | 4.00e+00 | + | | armature | panda_joint5 | 4 | 0.00e+00 | Not Specified | 0.00e+00 | + | | | panda_joint6 | 5 | 0.00e+00 | Not Specified | 0.00e+00 | + | | | panda_joint7 | 6 | 0.00e+00 | Not Specified | 0.00e+00 | + | | friction | panda_joint5 | 4 | 0.00e+00 | Not Specified | 0.00e+00 | + | | | panda_joint6 | 5 | 0.00e+00 | Not Specified | 0.00e+00 | + | | | panda_joint7 | 6 | 0.00e+00 | Not Specified | 0.00e+00 | + | panda_hand | joint_velocity_limit | panda_finger_joint1 | 7 | 2.00e-01 | Not Specified | 2.00e-01 | + | | | panda_finger_joint2 | 8 | 2.00e-01 | Not Specified | 2.00e-01 | + | | stiffness | panda_finger_joint1 | 7 | 1.00e+06 | 2.00e+03 | 2.00e+03 | + | | | panda_finger_joint2 | 8 | 1.00e+06 | 2.00e+03 | 2.00e+03 | + | | armature | panda_finger_joint1 | 7 | 0.00e+00 | Not Specified | 0.00e+00 | + | | | panda_finger_joint2 | 8 | 0.00e+00 | Not Specified | 0.00e+00 | + | | friction | panda_finger_joint1 | 7 | 0.00e+00 | Not Specified | 0.00e+00 | + | | | panda_finger_joint2 | 8 | 0.00e+00 | Not Specified | 0.00e+00 | + +----------------+------------------------+---------------------+----+-------------+--------------------+----------+ To keep the cleaniness of logging, :attr:`~isaaclab.assets.ArticulationCfg.actuator_value_resolution_debug_print` default to False, remember to turn it on when wishes. diff --git a/docs/source/migration/migrating_from_isaacgymenvs.rst b/docs/source/migration/migrating_from_isaacgymenvs.rst index 9edc874a693b..5b30c2027ed4 100644 --- a/docs/source/migration/migrating_from_isaacgymenvs.rst +++ b/docs/source/migration/migrating_from_isaacgymenvs.rst @@ -286,13 +286,13 @@ including file path, simulation parameters, actuator properties, and initial sta actuators={ "cart_actuator": ImplicitActuatorCfg( joint_names_expr=["slider_to_cart"], - effort_limit=400.0, - velocity_limit=100.0, + joint_effort_limit=400.0, + joint_velocity_limit=100.0, stiffness=0.0, damping=10.0, ), "pole_actuator": ImplicitActuatorCfg( - joint_names_expr=["cart_to_pole"], effort_limit=400.0, velocity_limit=100.0, stiffness=0.0, damping=0.0 + joint_names_expr=["cart_to_pole"], joint_effort_limit=400.0, joint_velocity_limit=100.0, stiffness=0.0, damping=0.0 ), }, ) @@ -726,14 +726,14 @@ the need to set simulation parameters for actors in the task implementation. | asset_root = os.path.dirname(asset_path) | actuators={ | | asset_file = os.path.basename(asset_path) | "cart_actuator": ImplicitActuatorCfg( | | | joint_names_expr=["slider_to_cart"], | -| asset_options = gymapi.AssetOptions() | effort_limit_sim=400.0, | -| asset_options.fix_base_link = True | velocity_limit_sim=100.0, | +| asset_options = gymapi.AssetOptions() | joint_effort_limit=400.0, | +| asset_options.fix_base_link = True | joint_velocity_limit=100.0, | | cartpole_asset = self.gym.load_asset(self.sim, | stiffness=0.0, | | asset_root, asset_file, asset_options) | damping=10.0, | | self.num_dof = self.gym.get_asset_dof_count( | ), | | cartpole_asset) | "pole_actuator": ImplicitActuatorCfg( | | | joint_names_expr=["cart_to_pole"], | -| pose = gymapi.Transform() | effort_limit_sim=400.0, velocity_limit_sim=100.0, | +| pose = gymapi.Transform() | joint_effort_limit=400.0, joint_velocity_limit=100.0, | | if self.up_axis == 'z': | stiffness=0.0, damping=0.0 | | pose.p.z = 2.0 | ), | | pose.r = gymapi.Quat(0.0, 0.0, 0.0, 1.0) | }, | @@ -776,32 +776,32 @@ collecting states, computing dones, calculating rewards, performing resets, and This workflow is done automatically by the framework such that a ``post_physics_step`` API is not required in the task. However, individual tasks can override the ``step()`` API to control the workflow. -+------------------------------------------------------------------+-------------------------------------------------------------+ -| IsaacGymEnvs | IsaacLab | -+------------------------------------------------------------------+-------------------------------------------------------------+ -|.. code-block:: python |.. code-block:: python | -| | | -| def pre_physics_step(self, actions): | def _pre_physics_step(self, actions: torch.Tensor) -> None: | -| actions_tensor = torch.zeros( | self.actions = self.action_scale * actions | -| self.num_envs * self.num_dof, | | -| device=self.device, dtype=torch.float) | def _apply_action(self) -> None: | -| actions_tensor[::self.num_dof] = actions.to( | self.cartpole.set_joint_effort_target( | -| self.device).squeeze() * self.max_push_effort | self.actions, joint_ids=self._cart_dof_idx) | -| forces = gymtorch.unwrap_tensor(actions_tensor) | | -| self.gym.set_dof_actuation_force_tensor( | | -| self.sim, forces) | | -| | | -| def post_physics_step(self): | | -| self.progress_buf += 1 | | -| | | -| env_ids = self.reset_buf.nonzero( | | -| as_tuple=False).squeeze(-1) | | -| if len(env_ids) > 0: | | -| self.reset_idx(env_ids) | | -| | | -| self.compute_observations() | | -| self.compute_reward() | | -+------------------------------------------------------------------+-------------------------------------------------------------+ ++-------------------------------------------------------+--------------------------------------------------------------+ +| IsaacGymEnvs | IsaacLab | ++-------------------------------------------------------+--------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def pre_physics_step(self, actions): | def _pre_physics_step(self, actions: torch.Tensor) -> None: | +| actions_tensor = torch.zeros( | self.actions = self.action_scale * actions | +| self.num_envs * self.num_dof, | | +| device=self.device, dtype=torch.float) | def _apply_action(self) -> None: | +| actions_tensor[::self.num_dof] = actions.to( | self.cartpole.actuators.target_command.set_effort_index( | +| self.device).squeeze() * self.max_push_effort | value=self.actions, joint_ids=self._cart_dof_idx) | +| forces = gymtorch.unwrap_tensor(actions_tensor) | | +| self.gym.set_dof_actuation_force_tensor( | | +| self.sim, forces) | | +| | | +| def post_physics_step(self): | | +| self.progress_buf += 1 | | +| | | +| env_ids = self.reset_buf.nonzero( | | +| as_tuple=False).squeeze(-1) | | +| if len(env_ids) > 0: | | +| self.reset_idx(env_ids) | | +| | | +| self.compute_observations() | | +| self.compute_reward() | | ++-------------------------------------------------------+--------------------------------------------------------------+ Dones and Resets diff --git a/docs/source/migration/migrating_from_omniisaacgymenvs.rst b/docs/source/migration/migrating_from_omniisaacgymenvs.rst index 872eaf340e59..e59de5a0be84 100644 --- a/docs/source/migration/migrating_from_omniisaacgymenvs.rst +++ b/docs/source/migration/migrating_from_omniisaacgymenvs.rst @@ -295,13 +295,13 @@ including file path, simulation parameters, actuator properties, and initial sta actuators={ "cart_actuator": ImplicitActuatorCfg( joint_names_expr=["slider_to_cart"], - effort_limit_sim=400.0, - velocity_limit_sim=100.0, + joint_effort_limit=400.0, + joint_velocity_limit=100.0, stiffness=0.0, damping=10.0, ), "pole_actuator": ImplicitActuatorCfg( - joint_names_expr=["cart_to_pole"], effort_limit=400.0, velocity_limit=100.0, stiffness=0.0, damping=0.0 + joint_names_expr=["cart_to_pole"], joint_effort_limit=400.0, joint_velocity_limit=100.0, stiffness=0.0, damping=0.0 ), }, ) @@ -539,18 +539,18 @@ Task config in Isaac Lab can be split into the main task configuration class and | gpu_max_rigid_patch_count: 81920 | actuators={ | | gpu_found_lost_pairs_capacity: 1024 | "cart_actuator": ImplicitActuatorCfg( | | gpu_found_lost_aggregate_pairs_capacity: 262144 | joint_names_expr=["slider_to_cart"], | -| gpu_total_aggregate_pairs_capacity: 1024 | effort_limit=400.0, | -| gpu_max_soft_body_contacts: 1048576 | velocity_limit=100.0, | +| gpu_total_aggregate_pairs_capacity: 1024 | joint_effort_limit=400.0, | +| gpu_max_soft_body_contacts: 1048576 | joint_velocity_limit=100.0, | | gpu_max_particle_contacts: 1048576 | stiffness=0.0, | | gpu_heap_capacity: 67108864 | damping=10.0, | | gpu_temp_buffer_capacity: 16777216 | ), | | gpu_max_num_partitions: 8 | "pole_actuator": ImplicitActuatorCfg( | -| | joint_names_expr=["cart_to_pole"], effort_limit=400.0, | -| Cartpole: | velocity_limit=100.0, stiffness=0.0, damping=0.0 | -| override_usd_defaults: False | ), | -| enable_self_collisions: False | }, | -| enable_gyroscopic_forces: True | ) | -| solver_position_iteration_count: 4 | | +| | joint_names_expr=["cart_to_pole"], | +| Cartpole: | joint_effort_limit=400.0, joint_velocity_limit=100.0, | +| override_usd_defaults: False | stiffness=0.0, damping=0.0 | +| enable_self_collisions: False | ), | +| enable_gyroscopic_forces: True | }, | +| solver_position_iteration_count: 4 | ) | | solver_velocity_iteration_count: 0 | | | sleep_threshold: 0.005 | | | stabilization_threshold: 0.001 | | @@ -661,33 +661,33 @@ Note that resets are no longer performed in the ``pre_physics_step`` API. In add ``_pre_physics_step`` and ``_apply_action`` methods allow for more flexibility in processing the action buffer and setting actions into simulation. -+------------------------------------------------------------------+-------------------------------------------------------------+ -| OmniIsaacGymEnvs | IsaacLab | -+------------------------------------------------------------------+-------------------------------------------------------------+ -|.. code-block:: python |.. code-block:: python | -| | | -| def pre_physics_step(self, actions) -> None: | def _pre_physics_step(self, | -| if not self.world.is_playing(): | actions: torch.Tensor) -> None: | -| return | self.actions = self.action_scale * actions | -| | | -| reset_env_ids = self.reset_buf.nonzero( | def _apply_action(self) -> None: | -| as_tuple=False).squeeze(-1) | self.cartpole.set_joint_effort_target( | -| if len(reset_env_ids) > 0: | self.actions, joint_ids=self._cart_dof_idx) | -| self.reset_idx(reset_env_ids) | | -| | | -| actions = actions.to(self._device) | | -| | | -| forces = torch.zeros((self._cartpoles.count, | | -| self._cartpoles.num_dof), | | -| dtype=torch.float32, device=self._device) | | -| forces[:, self._cart_dof_idx] = | | -| self._max_push_effort * actions[:, 0] | | -| | | -| indices = torch.arange(self._cartpoles.count, | | -| dtype=torch.int32, device=self._device) | | -| self._cartpoles.set_joint_efforts( | | -| forces, indices=indices) | | -+------------------------------------------------------------------+-------------------------------------------------------------+ ++---------------------------------------------------+--------------------------------------------------------------+ +| OmniIsaacGymEnvs | IsaacLab | ++---------------------------------------------------+--------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def pre_physics_step(self, actions) -> None: | def _pre_physics_step(self, | +| if not self.world.is_playing(): | actions: torch.Tensor) -> None: | +| return | self.actions = self.action_scale * actions | +| | | +| reset_env_ids = self.reset_buf.nonzero( | def _apply_action(self) -> None: | +| as_tuple=False).squeeze(-1) | self.cartpole.actuators.target_command.set_effort_index( | +| if len(reset_env_ids) > 0: | value=self.actions, joint_ids=self._cart_dof_idx) | +| self.reset_idx(reset_env_ids) | | +| | | +| actions = actions.to(self._device) | | +| | | +| forces = torch.zeros((self._cartpoles.count, | | +| self._cartpoles.num_dof), | | +| dtype=torch.float32, device=self._device) | | +| forces[:, self._cart_dof_idx] = | | +| self._max_push_effort * actions[:, 0] | | +| | | +| indices = torch.arange(self._cartpoles.count, | | +| dtype=torch.int32, device=self._device) | | +| self._cartpoles.set_joint_efforts( | | +| forces, indices=indices) | | ++---------------------------------------------------+--------------------------------------------------------------+ Dones and Resets diff --git a/docs/source/migration/migrating_to_isaaclab_3-0.rst b/docs/source/migration/migrating_to_isaaclab_3-0.rst index 4d1debcc00dc..696f5c8a27e0 100644 --- a/docs/source/migration/migrating_to_isaaclab_3-0.rst +++ b/docs/source/migration/migrating_to_isaaclab_3-0.rst @@ -20,6 +20,113 @@ This guide covers the main breaking changes and deprecations you need to address from Isaac Lab 2.x to Isaac Lab 3.0. +.. _actuators-solver-limit-migration: + +Actuator effort and joint-limit names +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Actuator configurations now use joint-qualified names for solver limits. Update active +configurations to the canonical fields below. The former names remain accepted with a +``DeprecationWarning`` through the 3.x release line and will be removed in 4.0. + +.. list-table:: Actuator limit migration + :header-rows: 1 + :widths: 38 38 24 + + * - Deprecated configuration field + - Canonical configuration field + - Runtime owner + * - ``effort_limit`` + - ``actuator_effort_limit`` + - Actuator model (rated limit) + * - ``effort_limit_sim`` + - ``joint_effort_limit`` + - :attr:`~isaaclab.assets.ArticulationData.joint_effort_limits` + * - ``velocity_limit_sim`` + - ``joint_velocity_limit`` + - :attr:`~isaaclab.assets.ArticulationData.joint_vel_limits` + +``actuator_effort_limit`` clips explicit actuator-model output. ``joint_effort_limit`` and +``joint_velocity_limit`` are construction-time joint-property overrides selected by an actuator +group's joint expression. The deprecated aliases ``effort_limit``, ``velocity_limit``, +``effort_limit_sim``, and ``velocity_limit_sim`` remain accepted through 3.x. ``effort_limit`` +resolves to the rated ``actuator_effort_limit`` for every actuator type. For an implicit group +without a separately configured solver clamp, the rated value also populates +``joint_effort_limit`` for backward compatibility; configure both fields to author distinct +rated and solver limits. The runtime ``effort_limit`` and ``velocity_limit`` group properties +follow the same mapping and are also deprecated. +``actuator_velocity_limit`` describes rated speed or an implicit soft-limit snapshot. +``joint_velocity_limit`` only requests solver enforcement, which is backend-dependent. See +:ref:`actuators-joint-property-ownership` for the full ownership model. + +**Behavior change — explicit groups keep the solver effort limit.** Isaac Lab previously raised the +solver effort limit to ``1.0e9`` on joints driven by an explicit actuator so that only the model +clipped the effort. The solver now keeps the authored or configured ``joint_effort_limit``, so +effort submitted by an explicit model is clipped a second time by the solver. If your asset authors +a tight joint effort limit and your policy relies on the model limit alone, set +``joint_effort_limit`` at least as large as ``actuator_effort_limit`` in the actuator +configuration. + +The runtime group properties listed below were removed. Read their live values from articulation +data and use the corresponding indexed articulation writer: + +.. list-table:: Removed group-property migration + :header-rows: 1 + :widths: 26 38 36 + + * - Removed runtime group property + - Read + - Write + * - ``effort_limit_sim`` + - :attr:`~isaaclab.assets.ArticulationData.joint_effort_limits` + - :meth:`~isaaclab.assets.Articulation.write_joint_effort_limit_to_sim_index` + * - ``velocity_limit_sim`` + - :attr:`~isaaclab.assets.ArticulationData.joint_vel_limits` + - :meth:`~isaaclab.assets.Articulation.write_joint_velocity_limit_to_sim_index` + * - ``armature`` + - :attr:`~isaaclab.assets.ArticulationData.joint_armature` + - :meth:`~isaaclab.assets.Articulation.write_joint_armature_to_sim_index` + * - ``friction`` + - :attr:`~isaaclab.assets.ArticulationData.joint_friction_coeff` + - :meth:`~isaaclab.assets.Articulation.write_joint_friction_coefficient_to_sim_index` + * - ``dynamic_friction`` + - ``data.joint_dynamic_friction_coeff`` (PhysX and OVPhysX) + - ``write_joint_dynamic_friction_coefficient_to_sim_index`` (PhysX and OVPhysX) + * - ``viscous_friction`` + - ``data.joint_viscous_friction_coeff`` + - ``write_joint_viscous_friction_coefficient_to_sim_index`` + +The dynamic-friction view and writer are backend-specific; Newton has no corresponding joint +property. + +**Custom actuator models.** The protected helper ``ActuatorBase._parse_joint_parameter`` was +removed together with the constructor rework. Custom actuator subclasses that parsed configuration +fields with it should call :func:`~isaaclab.actuators.resolve_joint_parameter`, which applies the +same resolution semantics as a standalone function: + +.. code-block:: python + + from isaaclab.actuators import ActuatorBase, resolve_joint_parameter + + + class MyActuator(ActuatorBase): + def __init__(self, cfg, joint_names, joint_ids, num_envs, device, **kwargs): + super().__init__(cfg, joint_names, joint_ids, num_envs, device, **kwargs) + # before: self.my_gain = self._parse_joint_parameter(cfg.my_gain, 0.0) + self.my_gain = resolve_joint_parameter(cfg.my_gain, 0.0, joint_names, num_envs, device) + +The backend articulation methods ``write_actuator_stiffness_to_sim`` and +``write_actuator_damping_to_sim`` are deprecated. Use +:func:`~isaaclab.envs.mdp.events.randomize_actuator_gains` for managed gain randomization; it +updates actuator-owned gains, implicit solver drives, or native-controller parameters as +appropriate. For direct writes to a Newton-executed group's controller, use +:func:`~isaaclab.actuators.newton.write_group_parameter`. + +Named regular-expression groups retain their configuration behavior. If both a deprecated name and +its canonical replacement are present in the same group, use only the canonical name; equivalent +values warn and select the canonical value, whereas conflicting values raise :class:`ValueError`. + + Visualizer CLI and Headless Behavior ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1080,6 +1187,123 @@ and OvPhysX bindings use different access methods. See before using ``root_view`` in backend-portable code. +Actuator API Moves to ``ActuatorCollection`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In Isaac Lab 3.x, actuator ownership moves from :class:`~isaaclab.assets.Articulation` to a +backend-neutral :class:`~isaaclab.actuators.ActuatorCollection`, available as +:attr:`~isaaclab.assets.Articulation.actuators`. Actuator command setters and per-joint actuator +telemetry now live on the collection, so the same code path drives every physics backend. The +collection setters are keyword-only. + + +Method Relocations +------------------ + +The following methods on :class:`~isaaclab.assets.Articulation` move to the actuator collection. +The old methods are deprecated and will be removed in a future release: + ++-------------------------------------------------------------+------------------------------------------------------+ +| Deprecated | New | ++=============================================================+======================================================+ +| ``set_joint_position_target`` | ``actuators.target_command.set_position_index`` | ++-------------------------------------------------------------+------------------------------------------------------+ +| ``set_joint_velocity_target`` | ``actuators.target_command.set_velocity_index`` | ++-------------------------------------------------------------+------------------------------------------------------+ +| ``set_joint_effort_target`` | ``actuators.target_command.set_effort_index`` | ++-------------------------------------------------------------+------------------------------------------------------+ +| ``set_joint_{position,velocity,effort}_target_index/_mask`` | ``actuators.target_command.set_{position,velocity,`` | +| | ``effort}_index/_mask`` | ++-------------------------------------------------------------+------------------------------------------------------+ + +Property Relocations (Data Class) +--------------------------------- + +The following properties on :class:`~isaaclab.assets.ArticulationData` move to the actuator +collection under the command view. The old properties are deprecated and will be removed in a +future release: + ++------------------------------+---------------------------------------+ +| Deprecated | New | ++==============================+=======================================+ +| ``data.joint_pos_target`` | ``actuators.target_command.position`` | ++------------------------------+---------------------------------------+ +| ``data.joint_vel_target`` | ``actuators.target_command.velocity`` | ++------------------------------+---------------------------------------+ +| ``data.joint_effort_target`` | ``actuators.target_command.effort`` | ++------------------------------+---------------------------------------+ +| ``data.computed_torque`` | ``actuators.computed_effort`` | ++------------------------------+---------------------------------------+ +| ``data.applied_torque`` | ``actuators.applied_effort`` | ++------------------------------+---------------------------------------+ + +.. note:: + + All deprecated methods and properties are forwarders that emit a :class:`DeprecationWarning` + when used. Your existing code will continue to work, but you should migrate to the new API to + avoid issues in future releases. + + :attr:`~isaaclab.assets.ArticulationData.soft_joint_vel_limits` remains on + :class:`~isaaclab.assets.ArticulationData`; do not migrate it to the actuator collection. + ``ArticulationData.gear_ratio`` was removed: it was legacy :class:`~isaaclab.actuators.DCMotor` + telemetry that was no longer updated and always read one. Gear ratios are an actuator + configuration input, not simulation output; read them from your actuator configuration. + +.. important:: + + LEAPP-exported action terms are a temporary exception. The collection command setters do not + yet carry LEAPP output annotations, so exportable terms must continue to call the deprecated, + annotated ``Articulation.set_joint_*_target_index`` or ``*_mask`` methods until collection + setters are supported by the exporter. Runtime code that is not exported should use the + collection API. + +Actuator group topology is configuration-time state. Add or remove a group on +:attr:`~isaaclab.assets.ArticulationCfg.actuators` before creating the articulation: + +.. code-block:: python + + robot_cfg.actuators["gripper"] = ImplicitActuatorCfg(...) + robot = Articulation(robot_cfg) + +At runtime, assignment to or deletion from ``robot.actuators`` raises :class:`TypeError`. Group +membership, joint coverage, native binding, execution slices, and cached launches are +construction-time invariants. Continue to use the public named groups and collection views; private +execution and compatibility-projection details are not migration targets. + + +Migration Example +----------------- + +Here's a complete example showing how to update your code: + +**Before (Isaac Lab 2.x):** + +.. code-block:: python + + # Setting joint targets on the articulation (deprecated) + robot = scene["robot"] + robot.set_joint_effort_target(efforts, joint_ids=joint_ids) + + # Reading actuator telemetry from the data class (deprecated) + applied = robot.data.applied_torque + pos_target = robot.data.joint_pos_target + +**After (Isaac Lab 3.0):** + +.. code-block:: python + + # Sending actuator commands expressed in joint-side coordinates (keyword-only) + robot = scene["robot"] + robot.actuators.target_command.set_effort_index(value=efforts, joint_ids=joint_ids) + + # Reading actuator telemetry from the collection + applied = robot.actuators.applied_effort.torch + position_command = robot.actuators.target_command.position.torch + +For the full runtime API of the actuator collection -- command setters and telemetry buffers -- +see :ref:`actuators-runtime-api`. + + Quaternion Format ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/overview/core-concepts/actuators.rst b/docs/source/overview/core-concepts/actuators.rst deleted file mode 100644 index de34e4202868..000000000000 --- a/docs/source/overview/core-concepts/actuators.rst +++ /dev/null @@ -1,100 +0,0 @@ -.. _overview-actuators: - - -Actuators -========= - -An articulated system comprises of actuated joints, also called the degrees of freedom (DOF). -In a physical system, the actuation typically happens either through active components, such as -electric or hydraulic motors, or passive components, such as springs. These components can introduce -certain non-linear characteristics which includes delays or maximum producible velocity or torque. - -In simulation, the joints are either position, velocity, or torque-controlled. For position and velocity -control, the physics engine internally implements a spring-damp (PD) controller which computes the torques -applied on the actuated joints. In torque-control, the commands are set directly as the joint efforts. -While this mimics an ideal behavior of the joint mechanism, it does not truly model how the drives work -in the physical world. Thus, we provide a mechanism to inject external models to compute the -joint commands that would represent the physical robot's behavior. - -Actuator models ---------------- - -We name two different types of actuator models: - -1. **implicit**: corresponds to the ideal simulation mechanism (provided by physics engine). -2. **explicit**: corresponds to external drive models (implemented by user). - -The explicit actuator model performs two steps: 1) it computes the desired joint torques for tracking -the input commands, and 2) it clips the desired torques based on the motor capabilities. The clipped -torques are the desired actuation efforts that are set into the simulation. - -As an example of an ideal explicit actuator model, we provide the :class:`isaaclab.actuators.IdealPDActuator` -class, which implements a PD controller with feed-forward effort, and simple clipping based on the configured -maximum effort: - -.. math:: - - \tau_{j, computed} & = k_p * (q_{des} - q) + k_d * (\dot{q}_{des} - \dot{q}) + \tau_{ff} \\ - \tau_{j, max} & = \gamma \times \tau_{motor, max} \\ - \tau_{j, applied} & = clip(\tau_{computed}, -\tau_{j, max}, \tau_{j, max}) - - -where, :math:`k_p` and :math:`k_d` are joint stiffness and damping gains, :math:`q` and :math:`\dot{q}` -are the current joint positions and velocities, :math:`q_{des}`, :math:`\dot{q}_{des}` and :math:`\tau_{ff}` -are the desired joint positions, velocities and torques commands. The parameters :math:`\gamma` and -:math:`\tau_{motor, max}` are the gear box ratio and the maximum motor effort possible. - -Actuator groups ---------------- - -The actuator models by themselves are computational blocks that take as inputs the desired joint commands -and output the joint commands to apply into the simulator. They do not contain any knowledge about the -joints they are acting on themselves. These are handled by the :class:`isaaclab.assets.Articulation` -class, which wraps around the physics engine's articulation class. - -Actuator are collected as a set of actuated joints on an articulation that are using the same actuator model. -For instance, the quadruped, ANYmal-C, uses series elastic actuator, ANYdrive 3.0, for all its joints. This -grouping configures the actuator model for those joints, translates the input commands to the joint level -commands, and returns the articulation action to set into the simulator. Having an arm with a different -actuator model, such as a DC motor, would require configuring a different actuator group. - -The following figure shows the actuator groups for a legged mobile manipulator: - -.. image:: ../../_static/actuator-group/actuator-light.svg - :class: only-light - :align: center - :alt: Actuator models for a legged mobile manipulator - :width: 80% - -.. image:: ../../_static/actuator-group/actuator-dark.svg - :class: only-dark - :align: center - :width: 80% - :alt: Actuator models for a legged mobile manipulator - -.. seealso:: - - We provide implementations for various explicit actuator models. These are detailed in - `isaaclab.actuators <../../api/lab/isaaclab.actuators.html>`_ sub-package. - -Considerations when using actuators ------------------------------------ - -As explained in the previous sections, there are two main types of actuator models: implicit and explicit. -The implicit actuator model is provided by the physics engine. This means that when the user sets either -a desired position or velocity, the physics engine will internally compute the efforts that need to be -applied to the joints to achieve the desired behavior. In PhysX, the PD controller adds numerical damping -to the desired effort, which results in more stable behavior. - -The explicit actuator model is provided by the user. This means that when the user sets either a desired -position or velocity, the user's model will compute the efforts that need to be applied to the joints to -achieve the desired behavior. While this provides more flexibility, it can also lead to some numerical -instabilities. One way to mitigate this is to use the ``armature`` parameter of the actuator model, either in -the USD file or in the articulation config. This parameter is used to dampen the joint response and helps -improve the numerical stability of the simulation. More details on how to improve articulation stability -can be found in the `OmniPhysics documentation `_. - -What does this mean for the user? It means that policies trained with implicit actuators may not transfer -to the exact same robot model when using explicit actuators. If you are running into issues like this, or -in cases where policies do not converge on explicit actuators while they do on implicit ones, increasing -or setting the ``armature`` parameter to a higher value may help. diff --git a/docs/source/overview/core-concepts/index.rst b/docs/source/overview/core-concepts/index.rst index 8c1327c4b140..8ce2b01f6aa0 100644 --- a/docs/source/overview/core-concepts/index.rst +++ b/docs/source/overview/core-concepts/index.rst @@ -11,7 +11,6 @@ This section we introduce core concepts in Isaac Lab. physical-backends/index schema_cfgs task_workflows - actuators sensors/index.rst renderers motion_generators diff --git a/docs/source/overview/core-concepts/motion_generators.rst b/docs/source/overview/core-concepts/motion_generators.rst index e4e09f2f17db..6f782c238c11 100644 --- a/docs/source/overview/core-concepts/motion_generators.rst +++ b/docs/source/overview/core-concepts/motion_generators.rst @@ -37,6 +37,11 @@ broadly categorized into: Joint-space controllers ----------------------- +There is no dedicated configuration class for selecting a joint-space control mode. Instead, the +control mode follows from which targets you set through ``articulation.actuators``: effort targets +for torque control, velocity targets for velocity control, and position targets for position +control. See :ref:`actuators-runtime-api` for the runtime API. + Torque control ~~~~~~~~~~~~~~ @@ -49,9 +54,6 @@ joint torque commands, i.e. at every time-step, \tau = \tau_{des} -Thus, this control mode is achievable by setting the command type for the actuator group, via -the :class:`ActuatorControlCfg` class, to ``"t_abs"``. - Velocity control ~~~~~~~~~~~~~~~~ @@ -67,9 +69,6 @@ current and desired joint velocities. Based on input actions, the joint torques where :math:`k_d` are the gains parsed from configuration. -This control mode is achievable by setting the command type for the actuator group, via -the :class:`ActuatorControlCfg` class, to ``"v_abs"`` or ``"v_rel"``. - .. attention:: While performing velocity control, in many cases, gravity compensation is required to ensure better @@ -91,10 +90,7 @@ are zero). Based on the input actions, the joint torque commands are computed as where :math:`k_p` and :math:`k_d` are the gains parsed from configuration. -In its simplest above form, the control mode is achievable by setting the command type for the actuator group, -via the :class:`ActuatorControlCfg` class, to ``"p_abs"`` or ``"p_rel"``. - -However, a more complete formulation which considers the dynamics of the articulation would be: +A more complete formulation which considers the dynamics of the articulation would be: .. math:: diff --git a/docs/source/overview/core-concepts/physical-backends/joint_and_body_ordering.rst b/docs/source/overview/core-concepts/physical-backends/joint_and_body_ordering.rst index c67a12859d32..5802c6f4420e 100644 --- a/docs/source/overview/core-concepts/physical-backends/joint_and_body_ordering.rst +++ b/docs/source/overview/core-concepts/physical-backends/joint_and_body_ordering.rst @@ -260,7 +260,7 @@ diverge because of: * contact generation and resolution * friction * restitution -* actuator models and configuration +* :ref:`actuator models and configuration ` * integration method * timestep and substeps * solver convergence diff --git a/docs/source/overview/core-concepts/physical-backends/newton/migrating-assets-from-physx-to-newton.rst b/docs/source/overview/core-concepts/physical-backends/newton/migrating-assets-from-physx-to-newton.rst index 7646d7c47b3e..6b63aa2c78f9 100644 --- a/docs/source/overview/core-concepts/physical-backends/newton/migrating-assets-from-physx-to-newton.rst +++ b/docs/source/overview/core-concepts/physical-backends/newton/migrating-assets-from-physx-to-newton.rst @@ -192,17 +192,19 @@ incorrect collision geometry, or insufficient actuator effort. See :ref:`mjwarp- the complete tuning sequence. +.. _newton-velocity-limits: + Velocity limits distinction --------------------------- -``velocity_limit`` is the actuator's physical rated speed. Isaac Lab can use it in actuator or -task logic, observations, rewards, and terminations, but MJWarp does not parse it into the solver -model and does not enforce it during stepping. +``actuator_velocity_limit`` is the actuator's physical rated speed. Isaac Lab can use it in +actuator or task logic, observations, rewards, and terminations, but MJWarp does not parse it into +the solver model and does not enforce it during stepping. -``velocity_limit_sim`` requests a solver-side hard clamp and has no direct hardware counterpart. +``joint_velocity_limit`` requests a solver-side hard clamp and has no direct hardware counterpart. Isaac Lab always writes the value to Newton's ``Model.joint_velocity_limit``. The MJWarp solver drops that field instead of consuming it, while the Kamino solver honors it. Consequently, neither -``velocity_limit`` nor ``velocity_limit_sim`` prevents a joint from exceeding the requested speed +``velocity_limit`` nor ``joint_velocity_limit`` prevents a joint from exceeding the requested speed under ``physics=newton_mjwarp``. Do not treat these values as an MJWarp safety mechanism. When a task requires a rated-speed @@ -210,8 +212,9 @@ boundary, check it explicitly in observations or terminations and use physically limits, damping, armature, action scaling, rate limits, or controller clipping to keep the response well behaved. PhysX does consume its supported solver clamp; an overly tight PhysX clamp can make a velocity-limit termination unreachable and create a hidden transfer difference. -``effort_limit_sim`` is the simulated effort limit; use per-joint actuator or gearbox limits -rather than one oversized value for the whole robot. +``joint_effort_limit`` is the simulated effort limit. Use +``actuator_effort_limit`` to clip an explicit actuator model, and choose both limits from the +motor and gearbox rather than using one oversized value for the whole robot. Why MJWarp often needs more armature diff --git a/docs/source/policy_deployment/02_gear_assembly/gear_assembly_policy.rst b/docs/source/policy_deployment/02_gear_assembly/gear_assembly_policy.rst index c188f1a8b879..29df87a8c358 100644 --- a/docs/source/policy_deployment/02_gear_assembly/gear_assembly_policy.rst +++ b/docs/source/policy_deployment/02_gear_assembly/gear_assembly_policy.rst @@ -369,8 +369,8 @@ For the UR10e and Flexiv Rizon 4s deployments, we use an impedance controller in "arm": ImplicitActuatorCfg( joint_names_expr=["shoulder_pan_joint", "shoulder_lift_joint", "elbow_joint", "wrist_1_joint", "wrist_2_joint", "wrist_3_joint"], - effort_limit=87.0, # From UR10e specifications - velocity_limit=2.0, # From UR10e specifications + joint_effort_limit=87.0, # From UR10e specifications + joint_velocity_limit=2.0, # From UR10e specifications stiffness=800.0, # Calibrated to match real behavior damping=40.0, # Calibrated to match real behavior ), @@ -385,27 +385,27 @@ For the UR10e and Flexiv Rizon 4s deployments, we use an impedance controller in actuators = { "shoulder": ImplicitActuatorCfg( joint_names_expr=["joint[1-2]"], - effort_limit=123.0, velocity_limit=2.094, + joint_effort_limit=123.0, joint_velocity_limit=2.094, stiffness=6000.0, damping=108.4, ), "elbow": ImplicitActuatorCfg( joint_names_expr=["joint[3-4]"], - effort_limit=64.0, velocity_limit=2.443, + joint_effort_limit=64.0, joint_velocity_limit=2.443, stiffness=4200.0, damping=90.7, ), "wrist": ImplicitActuatorCfg( joint_names_expr=["joint[5-7]"], - effort_limit=39.0, velocity_limit=4.887, + joint_effort_limit=39.0, joint_velocity_limit=4.887, stiffness=1500.0, damping=54.2, ), "gripper_drive": ImplicitActuatorCfg( joint_names_expr=["finger_joint"], - effort_limit=2.0, velocity_limit=1.0, + joint_effort_limit=2.0, joint_velocity_limit=1.0, stiffness=2e3, damping=1e1, ), "gripper_passive": ImplicitActuatorCfg( joint_names_expr=[".*_knuckle_joint"], - effort_limit=1.0, velocity_limit=1.0, + joint_effort_limit=1.0, joint_velocity_limit=1.0, stiffness=0.0, damping=0.0, ), } diff --git a/docs/source/policy_deployment/04_reach/reach_policy.rst b/docs/source/policy_deployment/04_reach/reach_policy.rst index 1b75742e4564..88e0a007a108 100644 --- a/docs/source/policy_deployment/04_reach/reach_policy.rst +++ b/docs/source/policy_deployment/04_reach/reach_policy.rst @@ -199,8 +199,8 @@ Each robot has specific actuator configurations and workspace definitions. # Joints 1-2: Higher torque (123 Nm), lower speed "shoulder": ImplicitActuatorCfg( joint_names_expr=["joint[1-2]"], - effort_limit_sim=123.0, - velocity_limit_sim=2.094, # 120 deg/s + joint_effort_limit=123.0, + joint_velocity_limit=2.094, # 120 deg/s stiffness=6000.0, damping=108.5, friction=0.0, @@ -209,8 +209,8 @@ Each robot has specific actuator configurations and workspace definitions. # Joints 3-4: Medium torque (64 Nm), medium speed "elbow": ImplicitActuatorCfg( joint_names_expr=["joint[3-4]"], - effort_limit_sim=64.0, - velocity_limit_sim=2.443, # 140 deg/s + joint_effort_limit=64.0, + joint_velocity_limit=2.443, # 140 deg/s stiffness=4200.0, damping=90.7, friction=0.0, @@ -219,8 +219,8 @@ Each robot has specific actuator configurations and workspace definitions. # Joints 5-7: Lower torque (39 Nm), higher speed "wrist": ImplicitActuatorCfg( joint_names_expr=["joint[5-7]"], - effort_limit_sim=39.0, - velocity_limit_sim=4.887, # 280 deg/s + joint_effort_limit=39.0, + joint_velocity_limit=4.887, # 280 deg/s stiffness=1500.0, damping=54.2, friction=0.0, diff --git a/docs/source/setup/walkthrough/technical_env_design.rst b/docs/source/setup/walkthrough/technical_env_design.rst index 62a086343f9c..460cc1af13a0 100644 --- a/docs/source/setup/walkthrough/technical_env_design.rst +++ b/docs/source/setup/walkthrough/technical_env_design.rst @@ -126,7 +126,7 @@ The next thing our environment needs is the definitions for how to handle action self.actions = actions.clone() def _apply_action(self) -> None: - self.robot.set_joint_velocity_target(self.actions, joint_ids=self.dof_idx) + self.robot.actuators.target_command.set_velocity_index(value=self.actions, joint_ids=self.dof_idx) Here the act of applying actions to the robot in the environment is broken into two steps: ``_pre_physics_step`` and ``_apply_action``. The physics simulation is decimated with respect to querying the policy for actions, meaning that multiple physics steps may occur per action taken by the policy. @@ -153,7 +153,7 @@ When we talk about a scene entity like the robot, we can either be talking about of the robot on the stage. The ``ArticulationData`` contains the data for those individual clones. This includes things like various kinematic vectors (like ``root_com_lin_vel_b``) and reference vectors (like ``robot.data.FORWARD_VEC_B``). -Notice how in the ``_apply_action`` method, we are calling a method of ``self.robot`` which is a method of ``Articulation``. The actions being applied are in the form of a 2D tensor +Notice how in the ``_apply_action`` method, we are calling a method of ``self.robot.actuators``, the actuator collection of the ``Articulation``. The actions being applied are in the form of a 2D tensor of shape ``[num_envs, num_actions]``. We are applying actions to **all** robots on the stage at once! Here, when we need to get the observations, we need the body frame velocity for all robots on the stage, and so access ``self.robot.data`` to get that information. The ``root_com_lin_vel_b`` is a property of the ``ArticulationData`` that handles the conversion of the center-of-mass linear velocity from the world frame to the body frame for us. Finally, Isaac Lab expects the observations to be returned as a dictionary, with ``policy`` defining those observations for the policy model and ``critic`` defining those observations for diff --git a/docs/source/tutorials/01_assets/run_articulation.rst b/docs/source/tutorials/01_assets/run_articulation.rst index db6d58fc07fd..a572dd5e7125 100644 --- a/docs/source/tutorials/01_assets/run_articulation.rst +++ b/docs/source/tutorials/01_assets/run_articulation.rst @@ -80,18 +80,19 @@ Stepping the simulation Applying commands to the articulation involves two steps: -1. *Setting the joint targets*: This sets the desired joint position, velocity, or effort targets for the articulation. +1. *Setting actuator commands*: This provides desired position, velocity, or effort values to the actuator models in + joint-side coordinates. 2. *Writing the data to the simulation*: Based on the articulation's configuration, this step handles any - :ref:`actuation conversions ` and writes the converted values to the PhysX buffer. + :ref:`actuation conversions ` and writes the converted values to the simulation buffers. In this tutorial, we control the articulation using joint effort commands. For this to work, we need to set the articulation's stiffness and damping parameters to zero. This is done a-priori inside the cart-pole's pre-defined configuration object. -At every step, we randomly sample joint efforts and set them to the articulation by calling the -:meth:`Articulation.set_joint_effort_target` method. After setting the targets, we call the -:meth:`Articulation.write_data_to_sim` method to write the data to the PhysX buffer. Finally, we step -the simulation. +At every step, we randomly sample joint efforts and set them on the articulation's actuator collection +by calling ``robot.actuators.target_command.set_effort_index``. After setting the commands, +we call the :meth:`Articulation.write_data_to_sim` method to write the data to the simulation buffers. +Finally, we step the simulation. .. literalinclude:: ../../../../scripts/tutorials/01_assets/run_articulation.py :language: python diff --git a/docs/source/tutorials/03_envs/create_manager_base_env.rst b/docs/source/tutorials/03_envs/create_manager_base_env.rst index e7c5a9e97fe9..147f20fe5753 100644 --- a/docs/source/tutorials/03_envs/create_manager_base_env.rst +++ b/docs/source/tutorials/03_envs/create_manager_base_env.rst @@ -71,7 +71,7 @@ Defining actions ---------------- In the previous tutorial, we directly input the action to the cartpole using -the :meth:`assets.Articulation.set_joint_effort_target` method. In this tutorial, we will +``robot.actuators.target_command.set_effort_index``. In this tutorial, we will use the :class:`managers.ActionManager` to handle the actions. The action manager can comprise of multiple :class:`managers.ActionTerm`. Each action term diff --git a/manim-actuator-animations-handoff.md b/manim-actuator-animations-handoff.md new file mode 100644 index 000000000000..595e24c0d0fc --- /dev/null +++ b/manim-actuator-animations-handoff.md @@ -0,0 +1,164 @@ +# Handoff: Manim animations for the IsaacLab actuator documentation + +## Context + +You are producing conceptual animations for `docs/source/concepts/actuators.rst` in IsaacLab +(branch `antoine/tmp_articulation`, PR #6839, worktree +`/home/antoiner/Documents/IsaacLab/.worktrees/tmp-articulation`). The page already has two kinds +of media in `docs/source/_static/actuators/`: + +- Simulator clips (`*-clip.webp`): five-pendulum parameter sweeps rendered from the simulator. +- Static curve plots (`*-curve-light.png` / `*-curve-dark.png`): matplotlib step responses. + +What is missing is the conceptual/mathematical layer — how the machinery works — which is what +you will build with Manim (Manim Community Edition). Five animations, specified below. + +## Hard constraints + +- **Do not add Manim (or anything else) to the repository's dependency tree.** Render offline in + your own environment (e.g. a scratch venv with `manim` installed). +- **Do not commit rendered artifacts or scene scripts yet.** Put scene sources in a working + directory (e.g. `manim-src/` outside the repo or in your scratchpad) and rendered outputs next + to them. Hosting is being settled separately (likely + `https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/`, the server the walkthrough + webp clips already use); the doc integration happens after that decision. +- **Output format**: animated `.webp` (looping, no audio), matching the existing clips. Target + ≤ 600 KB each at ~960px wide; the existing clips range 470 KB–1.2 MB and reviewers already + flagged size, so stay lean (short loops, limited palette, moderate frame rate ~24 fps). +- **Light and dark variants** of every animation, matching the repo convention + (`-light.*` / `-dark.*`). Existing figures use `:class: only-light` / + `:class: only-dark` in the RST. Dark background `#1a1a1a`-ish; check the existing + `*-curve-dark.png` files for the palette in use and match it. +- **Technical accuracy is non-negotiable.** Every equation and behavior shown must match the + implementation. Source-of-truth files are listed per animation; read them before animating. + Where this handoff and the code disagree, the code wins — and flag the discrepancy. +- No NVIDIA logos, no 3Blue1Brown branding; neutral style consistent with the existing figures. + +## The five animations + +### 1. `pipeline-flow` — the actuator pipeline (highest priority) + +Replaces/augments the static `pipeline-light.png` / `pipeline-dark.png` (see their alt text in +`actuators.rst` around lines 98–112 for the intended semantics). + +Animate a command pulse flowing through the architecture: + +1. User calls `actuators.target_command.set_position_index(...)` → pulse enters the + **ActuatorCollection** (joint-indexed staging buffers). +2. During `write_data_to_sim()`, the pulse splits into **three paths**: + - **Lab explicit model**: model computes torque on the host, clips it, submits effort to the + solver. Show `output_command` being filled on this path only. + - **Implicit drive**: targets pass through unchanged; a PD block *inside the solver box* + consumes them (gains live in the solver). + - **Native**: on Newton the actuator block sits *inside the solver* (CUDA-graph region); + on PhysX/OVPhysX a "host adapter" block processes during `write_data_to_sim()` and submits. +3. Telemetry (`computed_effort`, `applied_effort`) flows back to the collection; annotate that + native paths bypass `output_command`. + +Timing is the point: make it visible what happens during `write_data_to_sim()` versus inside the +solver step. Sources: `docs/source/concepts/actuators.rst` (sections "The actuator pipeline", +"Backend submission"), `source/isaaclab/isaaclab/actuators/actuator_collection.py` +(`ActuatorCollection.compute`). + +### 2. `implicit-vs-explicit-stability` — why explicit actuators diverge at high gains + +Two synchronized panels driving the same 1-DOF mass: + +- **Implicit**: PD evaluated continuously (solver-side) — smooth restoring force. +- **Explicit**: error sampled once per control step, torque held constant between samples + (zero-order hold). Draw the staircase torque against the continuous one. + +Start at moderate kp where both trajectories overlap; ramp kp until the explicit panel +oscillates and diverges while the implicit one stays stable. Annotate: "solver applies the +drive" vs "model evaluates PD once per step". End card: the practical rule from the doc — +policies trained on implicit gains may need adjustment for explicit actuators. + +Sources: `actuators.rst` "Implicit vs. explicit" section; `IdealPDActuator.compute` in +`source/isaaclab/isaaclab/actuators/actuator_pd.py`. The PD law shown must be +τ = k_p (q_des − q) + k_d (q̇_des − q̇) + τ_ff, clipped to ±`actuator_effort_limit`. + +### 3. `dc-motor-envelope` — the four-quadrant torque–speed envelope + +The static `velocity-limit-curve-*.png` shows the envelope; animate the **operating point** +living in it. Plane: velocity (x) vs torque (y). Draw the envelope from the three parameters: +`saturation_effort` (stall torque at q̇ = 0), `actuator_effort_limit` (flat cap), and +`actuator_velocity_limit` (no-load speed where available torque reaches zero). During a +simulated swing, show the *demanded* PD torque as a ghost point and the *applied* torque as its +projection onto the envelope — clipping is a projection, and the available torque shrinks as +speed rises. Cover at least two quadrants (motoring and braking). + +Sources: `DCMotor.compute` / `DCMotor._clip_effort` in +`source/isaaclab/isaaclab/actuators/actuator_pd.py` — **derive the exact envelope shape from +this code**, not from memory; the corner behavior (how the linear speed-dependent limit +intersects the flat effort cap, and the four-quadrant symmetry) must match. + +### 4. `effort-limit-damping-loss` — why saturation kills damping + +Companion to the existing `effort-limit-clip.webp` swing-up. Show the PD equation as three +signed stacked bars (kp term, kd term, τ_ff) summing to the demanded torque, next to a +horizontal ±τ_max band. When the stack exceeds the band, the *whole sum* is clipped — visually, +the damping contribution is eaten by saturation. Sync with a small pendulum inset that starts +oscillating exactly when the kd term is being lost, and settles once the demand re-enters the +band. This animates the doc paragraph: "When the PD demand exceeds the limit, the applied torque +and damping term are both clipped… An effort limit below the load's static demand prevents the +controller from damping the joint effectively." + +Sources: `actuators.rst` "Effort limit" section; `_clip_effort` in +`source/isaaclab/isaaclab/actuators/actuator_base.py`. Use the doc's numbers: ~2.94 N·m +gravity-hold torque, limits swept over [1, 2, 3, 4, 6] N·m if you show specific values. + +### 5. `delay-buffer` — the DelayedPDActuator circular buffer + +Left: a circular/FIFO buffer of N slots; commands (colored tokens) push in each physics step and +pop out `delay` steps later. Right: command-vs-response timeline for a square-wave position +target, response trailing by the delay. Then show a **reset**: the delay is re-sampled uniformly +from [`min_delay`, `max_delay`] and stays fixed until the next reset (annotate this — it is +per-reset randomization, not per-step noise). Optional end note from the doc: under native +execution the delay is fixed at `max_delay` (`min_delay` ignored). + +Sources: `DelayedPDActuator` in `source/isaaclab/isaaclab/actuators/actuator_pd.py`, +`DelayBuffer` in `source/isaaclab/isaaclab/utils/buffers/`; `actuators.rst` "Command delay" +section and the native-execution note near the end of the page. + +## Deliverable 0: the design charter (do this FIRST) + +Before animating, produce a short design charter (`animation-design-charter.md`) and build the +five scenes against it. It will be published alongside the docs (likely in the contributing +guide) so that every future animation is consistent with these and with the existing figures. +It must pin down, concretely (hex values, point sizes, seconds): + +- **Palette**: background, primary/secondary accents, per-role colors (command flow, torque, + velocity, limits/clipping, telemetry), for both light and dark variants. Derive from the + existing `*-curve-{light,dark}.png` figures so static plots and animations read as one family. +- **Typography**: one font for labels, one for math; minimum on-screen size at 960px width. +- **Layout**: margins, title placement, where equations live vs. where the "world" (pendulum, + buffer, envelope) lives; light/dark structural parity (same layout, only colors change). +- **Motion conventions**: standard durations (intro, emphasis pulse, loop length target + 6–12 s), easing, how clipping/saturation is always depicted (same visual metaphor everywhere), + how "inside the solver" vs "on the host" regions are drawn (this must match animation 1 and be + reused by all others). +- **Legibility rules**: max simultaneous moving elements, label-every-axis, units in brackets + per the docs convention (e.g. `[N·m]`, `[rad/s]`). + +The charter is a deliverable of equal weight to the animations: hand it back even if some +scenes are unfinished. + +## Naming and delivery + +- File names: `pipeline-flow-{light,dark}.webp`, `implicit-vs-explicit-stability-{light,dark}.webp`, + `dc-motor-envelope-{light,dark}.webp`, `effort-limit-damping-loss-{light,dark}.webp`, + `delay-buffer-{light,dark}.webp`. +- Deliver: the design charter, the rendered webp files, the Manim scene sources, and a one-line + render command per scene (so they can be regenerated), plus a short note per animation listing + which source files you verified the behavior against and anything where the doc and code + disagreed. +- Do not edit `actuators.rst` yet; the figure integration happens with the hosting decision. + +## Verification checklist before handing back + +- [ ] Design charter delivered; all five scenes conform to it. +- [ ] Every equation on screen matches the implementation (files listed above). +- [ ] DC-motor envelope derived from `DCMotor` code, including corner/quadrant behavior. +- [ ] Light/dark variants for all five; palette consistent with existing `*-dark.png` figures. +- [ ] Each webp ≤ ~600 KB, loops cleanly, readable at 960px and at 50% zoom. +- [ ] No repo modifications: no new dependencies, no committed artifacts. diff --git a/scripts/tutorials/01_assets/add_new_robot.py b/scripts/tutorials/01_assets/add_new_robot.py index 00af569307ff..0186b6f7f6ee 100644 --- a/scripts/tutorials/01_assets/add_new_robot.py +++ b/scripts/tutorials/01_assets/add_new_robot.py @@ -59,22 +59,22 @@ actuators={ "front_joints": ImplicitActuatorCfg( joint_names_expr=["joint[1-2]"], - effort_limit_sim=100.0, - velocity_limit_sim=100.0, + joint_effort_limit=100.0, + joint_velocity_limit=100.0, stiffness=10000.0, damping=100.0, ), "joint3_act": ImplicitActuatorCfg( joint_names_expr=["joint3"], - effort_limit_sim=100.0, - velocity_limit_sim=100.0, + joint_effort_limit=100.0, + joint_velocity_limit=100.0, stiffness=10000.0, damping=100.0, ), "joint4_act": ImplicitActuatorCfg( joint_names_expr=["joint4"], - effort_limit_sim=100.0, - velocity_limit_sim=100.0, + joint_effort_limit=100.0, + joint_velocity_limit=100.0, stiffness=10000.0, damping=100.0, ), diff --git a/scripts/tutorials/01_assets/run_articulation.py b/scripts/tutorials/01_assets/run_articulation.py index 6532909f97bd..5607d48d4d58 100644 --- a/scripts/tutorials/01_assets/run_articulation.py +++ b/scripts/tutorials/01_assets/run_articulation.py @@ -110,7 +110,7 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, Articula # -- generate random joint efforts efforts = torch.randn_like(robot.data.joint_pos.torch) * 5.0 # -- apply action to the robot - robot.set_joint_effort_target_index(target=efforts) + robot.actuators.target_command.set_effort_index(value=efforts) # -- write data to sim robot.write_data_to_sim() # Perform step diff --git a/scripts/tutorials/06_deploy/anymal_c_env.py b/scripts/tutorials/06_deploy/anymal_c_env.py index c3999cb5b8b5..96b1324b5f30 100644 --- a/scripts/tutorials/06_deploy/anymal_c_env.py +++ b/scripts/tutorials/06_deploy/anymal_c_env.py @@ -140,7 +140,7 @@ def _get_rewards(self) -> torch.Tensor: yaw_rate_error_mapped = torch.exp(-yaw_rate_error / 0.25) z_vel_error = torch.square(self._robot.data.root_lin_vel_b.torch[:, 2]) ang_vel_error = torch.sum(torch.square(self._robot.data.root_ang_vel_b.torch[:, :2]), dim=1) - joint_torques = torch.sum(torch.square(self._robot.data.applied_torque.torch), dim=1) + joint_torques = torch.sum(torch.square(self._robot.actuators.applied_effort.torch), dim=1) joint_accel = torch.sum(torch.square(self._robot.data.joint_acc.torch), dim=1) action_rate = torch.sum(torch.square(self._actions - self._previous_actions), dim=1) first_contact = self._contact_sensor.compute_first_contact(self.step_dt).torch[:, self._feet_ids] diff --git a/skills/user/migrate-2x-to-3x/SKILL.md b/skills/user/migrate-2x-to-3x/SKILL.md index 8bb8ab257694..5adcd14818d3 100644 --- a/skills/user/migrate-2x-to-3x/SKILL.md +++ b/skills/user/migrate-2x-to-3x/SKILL.md @@ -1,6 +1,6 @@ --- name: isaaclab-migrating-2x-to-3x -description: Migrates Isaac Lab 2.x projects to Isaac Lab 3.0 by routing agents through the official migration guide, current source APIs, and focused compatibility checks. Use when users mention Isaac Lab 3.0 migration, 2.x projects, quaternion order changes, ProxyArray data access, backend migration, Isaac Sim extension imports, or visualization CLI changes. +description: Migrates Isaac Lab 2.x projects to Isaac Lab 3.0 by routing agents through the official migration guide, current source APIs, and focused compatibility checks. Use when users mention Isaac Lab 3.0 migration, 2.x projects, actuator collection changes, quaternion order changes, ProxyArray data access, backend migration, Isaac Sim extension imports, or visualization CLI changes. audience: user status: experimental owners: @@ -18,7 +18,9 @@ Do not copy migration tables into answers from memory. Read the official migrati ## Workflow 1. Read the official migration guide in `docs/source/migration/migrating_to_isaaclab_3-0.rst`. -2. Identify which migration area applies: visualization CLI, backend packages, schema cfgs, quaternion order, `ProxyArray`, asset views, RSL-RL config, Isaac Sim extension enablement, or project-specific scripts. +2. Identify which migration area applies: visualization CLI, backend packages, schema cfgs, actuator collection + ownership and runtime topology, quaternion order, `ProxyArray`, asset views, RSL-RL config, Isaac Sim extension + enablement, or project-specific scripts. 3. Search the downstream project for old API symbols before editing. 4. For user code that imports Isaac Sim extension modules directly: - Prefer an Isaac Lab in-tree API when the migration guide lists one. diff --git a/skills/user/prepare-assets-for-newton/SKILL.md b/skills/user/prepare-assets-for-newton/SKILL.md index d01c3ff9c05d..9cec3f1bd5b8 100644 --- a/skills/user/prepare-assets-for-newton/SKILL.md +++ b/skills/user/prepare-assets-for-newton/SKILL.md @@ -23,7 +23,7 @@ Implicit MPM particle assets and rigid-MPM coupling are also out of scope; see t 2. **Use per-solver asset configuration classes.** Put common USD Physics properties in solver-common base cfgs. Put MJWarp-specific fields in `Mujoco*Cfg`, Newton-native fields in `Newton*Cfg`, and PhysX-only fields in `Physx*Cfg`. Confirm support in the generated schema APIs. 3. **Audit the authored mechanical model.** Check every dynamic link and contact-relevant object for intentional mass, COM, inertia and frames, collision geometry, approximation and scale, materials, articulation root, fixed-base and fixed-joint representation, joint axes and limits, self-collision, and gravity overrides. 4. **Match contact and friction behavior.** Expect more default slip in MJWarp. Validate colliders, material bindings, contact locations and gripper force. Set per-shape `condim` with `MujocoCollisionCfg`, tune material friction, then set global `MJWarpSolverCfg(cone=..., impratio=...)`. Treat `priority`, `solmix`, `solref`, and `solimp` as expert per-collider overrides. Use fixed-grasp displacement, contact count, effort, penetration, and success metrics. -5. **Velocity limits distinction.** `velocity_limit` is a rated speed and `velocity_limit_sim` is a requested solver clamp. Isaac Lab writes the latter to Newton's model, but MJWarp does not consume or enforce it; Kamino does. Add task or control checks for required speed bounds under MJWarp and use per-joint `effort_limit_sim`. +5. **Velocity limits distinction.** `velocity_limit` is a rated speed and `joint_velocity_limit` is a requested solver clamp. Isaac Lab writes the latter to Newton's model, but MJWarp does not consume or enforce it; Kamino does. Add task or control checks for required speed bounds under MJWarp. Use `joint_effort_limit` for solver limits and `actuator_effort_limit` to clip explicit actuator models. 6. **Why MJWarp often needs more armature.** Use reflected rotor inertia or controlled response tests for articulated coordinates. A plain rigid object has body inertia, not actuator armature. Correct mass, inertia, units, reset penetration, effort, action scale, control period, and contact capacity before changing armature. 7. **Retune damping with armature.** Increasing armature changes effective inertia and damping ratio. Tune armature, stiffness, and damping together from a step response, use conservative action scales, and keep targets away from hard stops to prevent bang-bang control. 8. **Choose an MJWarp starting profile.** Do not translate PhysX parameters numerically. Start from the nearest profile on the MJWarp solver page, keep the documented convergence defaults initially, enable `NewtonCfg.debug_mode`, and use MuJoCo contacts unless the task requires `NewtonCfg.collision_cfg`. Then run zero and random agents in PhysX and MJWarp through multiple resets. Check non-finite state, first-step impulses, saturation, excessive angular velocity, contact loss, and warnings. Reject penetrations, impossible mimic states, and invalid randomized geometry before stepping. diff --git a/skills/user/prepare-assets-for-newton/evaluations.md b/skills/user/prepare-assets-for-newton/evaluations.md index fa4edd21084d..afd32959dc10 100644 --- a/skills/user/prepare-assets-for-newton/evaluations.md +++ b/skills/user/prepare-assets-for-newton/evaluations.md @@ -43,7 +43,7 @@ Query: "Why does MJWarp exceed my joint velocity limit?" Expected behavior: -- Explain that MJWarp enforces neither `velocity_limit` nor `velocity_limit_sim`. +- Explain that MJWarp enforces neither `velocity_limit` nor `joint_velocity_limit`. - Recommend explicit task checks and physically justified control limits. Known failure modes: treating either field as an MJWarp safety clamp. diff --git a/skills/user/prepare-assets-for-newton/examples.md b/skills/user/prepare-assets-for-newton/examples.md index 4a7389bafb5b..eec7a01da304 100644 --- a/skills/user/prepare-assets-for-newton/examples.md +++ b/skills/user/prepare-assets-for-newton/examples.md @@ -19,7 +19,7 @@ metrics. Limit recursive spawner overrides to assets whose colliders should all ## Velocity Limit Is Exceeded -Treat `velocity_limit` as rated speed and `velocity_limit_sim` as a solver request. Because MJWarp enforces neither, add the required observation or termination check and tune effort, damping, armature, action scaling, rate limits, or controller clipping. +Treat `velocity_limit` as rated speed and `joint_velocity_limit` as a solver request. Because MJWarp enforces neither, add the required observation or termination check and tune effort, damping, armature, action scaling, rate limits, or controller clipping. ## Zero-Gravity Spin-Up diff --git a/skills/user/prepare-assets-for-newton/reference.md b/skills/user/prepare-assets-for-newton/reference.md index a6fb6fc5732d..d749bcea1014 100644 --- a/skills/user/prepare-assets-for-newton/reference.md +++ b/skills/user/prepare-assets-for-newton/reference.md @@ -58,8 +58,8 @@ Track fixed-grasp displacement, contact count, effort, penetration, success, con ## Velocity Limits Distinction -- `velocity_limit` is the actuator's rated speed; MJWarp does not parse or enforce it. -- `velocity_limit_sim` requests a solver clamp. Isaac Lab always writes it to Newton's `Model.joint_velocity_limit`; MJWarp drops the value when constructing its solver model, while Kamino honors it. +- `actuator_velocity_limit` is the actuator's rated speed; MJWarp does not parse or enforce it. +- `joint_velocity_limit` requests a solver clamp. Isaac Lab writes it to Newton's `Model.joint_velocity_limit`; MJWarp drops the value when constructing its solver model, while Kamino honors it. - Check required speed bounds in observations or terminations. Use effort limits, damping, armature, action scaling, rate limits, or controller clipping for well-behaved response. - PhysX can enforce its supported clamp, so a tight PhysX clamp can hide a task termination. diff --git a/skills/user/use-sensors-actuators/SKILL.md b/skills/user/use-sensors-actuators/SKILL.md index de4254487ce0..f9db187123d9 100644 --- a/skills/user/use-sensors-actuators/SKILL.md +++ b/skills/user/use-sensors-actuators/SKILL.md @@ -48,7 +48,11 @@ uv run --no-project python tools/skills/cli.py check ## Maintenance -Keep this skill synchronized with sensor and actuator docs under `docs/source/overview/core-concepts/`, sensor tutorials under `docs/source/tutorials/04_sensors/`, API docs under `docs/source/api/`, and maintained task examples under `source/isaaclab_tasks/isaaclab_tasks/`. If sensor support changes for PhysX, Newton, or renderers, update the docs or source examples first. +Keep this skill synchronized with actuator docs under `docs/source/concepts/`, sensor docs under +`docs/source/overview/core-concepts/sensors/`, sensor tutorials under +`docs/source/tutorials/04_sensors/`, API docs under `docs/source/api/`, and maintained task examples +under `source/isaaclab_tasks/isaaclab_tasks/`. If sensor support changes for PhysX, Newton, or +renderers, update the docs or source examples first. ## References @@ -57,7 +61,7 @@ Keep this skill synchronized with sensor and actuator docs under `docs/source/ov - [Add sensors on robot tutorial](../../../docs/source/tutorials/04_sensors/add_sensors_on_robot.rst) - [Contact sensor docs](../../../docs/source/overview/core-concepts/sensors/contact_sensor.rst) - [Joint wrench sensor docs](../../../docs/source/overview/core-concepts/sensors/joint_wrench_sensor.rst) -- [Actuators docs](../../../docs/source/overview/core-concepts/actuators.rst) +- [Actuators docs](../../../docs/source/concepts/actuators.rst) - [Sensors API](../../../docs/source/api/lab/isaaclab.sensors.rst) - [Sensor patterns API](../../../docs/source/api/lab/isaaclab.sensors.patterns.rst) - [Actuators API](../../../docs/source/api/lab/isaaclab.actuators.rst) diff --git a/source/isaaclab/changelog.d/actuator-collection.minor.rst b/source/isaaclab/changelog.d/actuator-collection.minor.rst new file mode 100644 index 000000000000..e2ad17231da1 --- /dev/null +++ b/source/isaaclab/changelog.d/actuator-collection.minor.rst @@ -0,0 +1,100 @@ +Added +^^^^^ + +* Added :class:`~isaaclab.actuators.ActuatorCollection` as the runtime + actuator API, with separate target-command, output-command, and telemetry + views (:attr:`~isaaclab.actuators.ActuatorCollection.target_command` and + :attr:`~isaaclab.actuators.ActuatorCollection.output_command`). +* Added execution aggregation for disjoint implicit actuator groups while + preserving named group configuration and access. Explicit actuator groups + execute one group at a time. +* Added ``actuator_effort_limit`` as the explicit actuator-model clipping + limit, alongside the canonical ``joint_effort_limit`` and + ``joint_velocity_limit`` joint-property overrides. +* Added ``isaaclab.actuators.newton`` hosting the Newton actuator adapter, + host runtime, and kernels shared by every backend's native execution path. +* Added :func:`~isaaclab.actuators.resolve_joint_parameter` as the public + joint-parameter resolution function shared by actuator models and the + collection. +* Added :func:`~isaaclab.actuators.newton.read_group_parameter` + and :func:`~isaaclab.actuators.newton.write_group_parameter` + as the single group-addressed access to Newton actuator parameters (for + example ``("controller", "kp")`` or ``("clamping", "max_effort")``), + implemented on Newton's selection API on every backend. + +Deprecated +^^^^^^^^^^ + +* Deprecated articulation-level actuator command setters and command and + torque-telemetry properties on articulation data. Use the ``target_command`` + view and ``computed_effort`` or ``applied_effort`` views on + :attr:`~isaaclab.assets.Articulation.actuators` instead. +* Deprecated Isaac Lab execution of explicit actuator models. Enable + :attr:`~isaaclab.sim.SimulationCfg.use_newton_actuators` to execute these + models through the native actuator path. +* Deprecated the actuator configuration aliases ``effort_limit``, + ``effort_limit_sim``, and ``velocity_limit_sim``, and the runtime group + property ``effort_limit``. Use + ``actuator_effort_limit`` for the rated actuator-model limit and + ``joint_effort_limit`` or ``joint_velocity_limit`` for solver limits. + Implicit groups may configure both to keep distinct rated and solver + effort limits. The aliases remain available through Isaac Lab 3.x and + will be removed in 4.0. +* Deprecated ``write_actuator_stiffness_to_sim`` and + ``write_actuator_damping_to_sim``. These backend-specific writers remain + available through 3.x; use + :func:`~isaaclab.envs.mdp.events.randomize_actuator_gains` for managed + randomization or + :func:`~isaaclab.actuators.newton.write_group_parameter` + for direct controller writes. + +Removed +^^^^^^^ + +* **Breaking:** Removed group-level ``effort_limit_sim``, ``velocity_limit_sim``, + ``armature``, ``friction``, ``dynamic_friction``, and ``viscous_friction`` + accessors. Read the corresponding :class:`~isaaclab.assets.ArticulationData` + joint property and use the articulation's ``write_joint_*_to_sim_index`` + writer instead. +* **Breaking:** Removed the protected helper ``ActuatorBase._parse_joint_parameter``. + Custom actuator subclasses should call + :func:`~isaaclab.actuators.resolve_joint_parameter` with the group's joint + names, environment count, and device instead. +* **Breaking:** Removed ``ArticulationData.gear_ratio`` and its backing buffers. + The property was legacy :class:`~isaaclab.actuators.DCMotor` telemetry that + was no longer updated by any execution path and always read one. Gear ratios + are an actuator configuration input; read them from your actuator + configuration instead. + +Changed +^^^^^^^ + +* Changed :class:`~isaaclab.actuators.ActuatorCollection` so named groups retain + their configuration and access identity while disjoint implicit groups can + share execution. +* Changed :attr:`~isaaclab.actuators.ImplicitActuatorCfg.velocity_limit` to + populate the actuator soft velocity-limit view. Use ``joint_velocity_limit`` + to configure the solver velocity clamp. +* Changed actuator joint-property overrides to write articulation-owned runtime + state. Read live limits, armature, and friction through + :class:`~isaaclab.assets.ArticulationData`; ordinary actuator groups retain + only actuator-model state. +* **Breaking:** Changed actuator collection membership to be fixed at + construction. Configure groups through + :attr:`~isaaclab.assets.ArticulationCfg.actuators` before constructing the + articulation; runtime assignment to or deletion from + :attr:`~isaaclab.assets.Articulation.actuators` raises :class:`TypeError`. +* **Breaking:** Rejected actuator configurations that assign a joint to more + than one group. Use disjoint joint-name expressions so each joint belongs to + at most one actuator group. +* **Breaking:** Changed explicit actuator groups to keep the authored solver + effort limit instead of widening it to ``1.0e9``. Effort submitted by an + explicit model is now also clipped by the solver's ``joint_effort_limit``; + configure it at least as large as ``actuator_effort_limit`` when the model + should be the only clip. + +Fixed +^^^^^ + +* Fixed runtime, play, and startup benchmarks to step environments under + PyTorch inference mode. diff --git a/source/isaaclab/changelog.d/newton-neural-checkpoint-resolution.rst b/source/isaaclab/changelog.d/newton-neural-checkpoint-resolution.rst new file mode 100644 index 000000000000..cfcd49822acf --- /dev/null +++ b/source/isaaclab/changelog.d/newton-neural-checkpoint-resolution.rst @@ -0,0 +1,5 @@ +Fixed +^^^^^ + +* Fixed Newton neural actuators failing to load actuator-network checkpoints + from remote paths. diff --git a/source/isaaclab/isaaclab/actuators/__init__.py b/source/isaaclab/isaaclab/actuators/__init__.py index b0dff8dafd51..90909810a825 100644 --- a/source/isaaclab/isaaclab/actuators/__init__.py +++ b/source/isaaclab/isaaclab/actuators/__init__.py @@ -18,8 +18,9 @@ - **Neural Network-based**: Learned motor models from actuator data. Every actuator model inherits from the :class:`isaaclab.actuators.ActuatorBase` class, -which defines the common interface for all actuator models. The actuator models are handled -and called by the :class:`isaaclab.assets.Articulation` class. +which defines the common interface for all actuator models. Runtime actuator groups, +commands, and telemetry are handled by :class:`isaaclab.actuators.ActuatorCollection`, +which is exposed through :attr:`isaaclab.assets.Articulation.actuators`. """ from isaaclab.utils.module import lazy_export diff --git a/source/isaaclab/isaaclab/actuators/__init__.pyi b/source/isaaclab/isaaclab/actuators/__init__.pyi index 566967cf1100..f626aa1e6639 100644 --- a/source/isaaclab/isaaclab/actuators/__init__.pyi +++ b/source/isaaclab/isaaclab/actuators/__init__.pyi @@ -6,6 +6,10 @@ __all__ = [ "ActuatorBase", "ActuatorBaseCfg", + "ActuatorTargetCommand", + "ActuatorCollection", + "ActuatorControl", + "ActuatorOutputCommand", "ActuatorNetLSTM", "ActuatorNetMLP", "ActuatorNetLSTMCfg", @@ -20,10 +24,13 @@ __all__ = [ "IdealPDActuatorCfg", "ImplicitActuatorCfg", "RemotizedPDActuatorCfg", + "resolve_joint_parameter", ] -from .actuator_base import ActuatorBase +from .actuator_base import ActuatorBase, resolve_joint_parameter from .actuator_base_cfg import ActuatorBaseCfg +from .actuator_collection import ActuatorCollection, ActuatorTargetCommand, ActuatorOutputCommand +from .actuator_control import ActuatorControl from .actuator_net import ActuatorNetLSTM, ActuatorNetMLP from .actuator_net_cfg import ActuatorNetLSTMCfg, ActuatorNetMLPCfg from .actuator_pd import ( diff --git a/source/isaaclab/isaaclab/actuators/_compat.py b/source/isaaclab/isaaclab/actuators/_compat.py new file mode 100644 index 000000000000..40fd212020d8 --- /dev/null +++ b/source/isaaclab/isaaclab/actuators/_compat.py @@ -0,0 +1,89 @@ +# 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 + +"""Deprecated actuator-configuration compatibility helpers. + +Everything in this module supports configuration fields and constructor +arguments that are deprecated in Isaac Lab 3.x and scheduled for removal in +4.0. Delete this module together with the aliases. +""" + +from __future__ import annotations + +import warnings +from typing import TYPE_CHECKING + +import torch + +from isaaclab.utils.string import _resolve_matching_values_dense + +from .actuator_base_cfg import _is_implicit_actuator_cfg + +if TYPE_CHECKING: + from .actuator_base_cfg import ActuatorBaseCfg + + +def _limits_equal(first: torch.Tensor | float, second: torch.Tensor | float) -> bool: + """Return whether two constructor limit arguments are equivalent.""" + if isinstance(first, torch.Tensor): + if isinstance(second, torch.Tensor): + return first.shape == second.shape and torch.equal(first, second) + return bool(torch.all(first == float(second)).item()) + if isinstance(second, torch.Tensor): + return bool(torch.all(second == float(first)).item()) + return float(first) == float(second) + + +def _resolve_limit_aliases( + actuator_name: str, + cfg: ActuatorBaseCfg, + joint_names: list[str], + *, + warn_deprecated: bool = True, +) -> None: + """Normalize deprecated effort- and velocity-limit aliases on an actuator configuration. + + Writes the configuration in place: canonical fields absorb the alias values after + equivalence validation and the alias fields are cleared. The collection resolves on + its internal configuration copies; directly constructed actuators resolve on the + configuration object they were given. + """ + implicit = _is_implicit_actuator_cfg(cfg) + if implicit and cfg.joint_effort_limit is None and cfg.effort_limit_sim is None and cfg.effort_limit is not None: + # Deprecated implicit behavior: without a separate solver clamp, the rated + # effort limit also reaches the solver. + cfg.joint_effort_limit = cfg.effort_limit + if implicit and cfg.actuator_velocity_limit is None and cfg.velocity_limit is None and cfg.velocity_limit_sim is not None: + # Deprecated implicit behavior: the solver clamp doubles as the soft joint + # velocity limit so the data buffers stay meaningful. + cfg.actuator_velocity_limit = cfg.velocity_limit_sim + + for new_name, old_name in ( + ("joint_effort_limit", "effort_limit_sim"), + ("actuator_effort_limit", "effort_limit"), + ("joint_velocity_limit", "velocity_limit_sim"), + ("actuator_velocity_limit", "velocity_limit"), + ): + alias_value = getattr(cfg, old_name) + if alias_value is None: + continue + if warn_deprecated: + warnings.warn( + f"Actuator group '{actuator_name}' uses deprecated '{old_name}'. Use " + f"'{new_name}' instead; '{old_name}' will be removed in 4.0.", + DeprecationWarning, + stacklevel=3, + ) + new_value = getattr(cfg, new_name) + if new_value is None: + setattr(cfg, new_name, alias_value) + elif _resolve_matching_values_dense(new_value, joint_names) != _resolve_matching_values_dense( + alias_value, joint_names + ): + raise ValueError( + f"Actuator group '{actuator_name}' has conflicting '{new_name}' and " + f"deprecated '{old_name}' values." + ) + setattr(cfg, old_name, None) diff --git a/source/isaaclab/isaaclab/actuators/actuator_base.py b/source/isaaclab/isaaclab/actuators/actuator_base.py index 965b759694b9..7805909299a4 100644 --- a/source/isaaclab/isaaclab/actuators/actuator_base.py +++ b/source/isaaclab/isaaclab/actuators/actuator_base.py @@ -5,6 +5,7 @@ from __future__ import annotations +import warnings from abc import ABC, abstractmethod from collections.abc import Sequence from typing import TYPE_CHECKING, ClassVar @@ -14,10 +15,77 @@ import isaaclab.utils.string as string_utils from isaaclab.utils.types import ArticulationActions +from ._compat import _limits_equal, _resolve_limit_aliases + if TYPE_CHECKING: from .actuator_base_cfg import ActuatorBaseCfg +def resolve_joint_parameter( + cfg_value: float | dict[str, float] | None, + default_value: float | torch.Tensor | None, + joint_names: list[str], + num_envs: int, + device: str, +) -> torch.Tensor: + """Resolve one group-shaped joint parameter from configuration and defaults. + + The single source of joint-parameter resolution semantics, shared by the actuator + models and by :class:`~isaaclab.actuators.ActuatorCollection` when it resolves the + construction-time joint properties. + + Args: + cfg_value: The parameter value from the configuration, a scalar or a + joint-name-pattern dictionary. If None, then the default value is used. + default_value: The default value, a scalar or a ``(num_envs, len(joint_names))`` + tensor. If it is also None, then an error is raised. + joint_names: The group's joint names, defining the column order. + num_envs: Number of articulation instances. + device: Torch device string. + + Returns: + The resolved parameter value, shape ``(num_envs, len(joint_names))``. + + Raises: + TypeError: If the parameter or default value is not of the expected type. + ValueError: If both values are None, or the default tensor has the wrong shape. + """ + num_joints = len(joint_names) + param = torch.zeros(num_envs, num_joints, device=device) + if cfg_value is not None: + if isinstance(cfg_value, (float, int, dict)): + dense_values = string_utils._resolve_matching_values_dense(cfg_value, joint_names) + param[:] = torch.tensor(dense_values, dtype=torch.float, device=device) + else: + raise TypeError( + f"Invalid type for parameter value: {type(cfg_value)} for " + + f"actuator on joints {joint_names}. Expected float or dict." + ) + elif default_value is not None: + if isinstance(default_value, (float, int)): + # if float, then use the same value for all joints + param[:] = float(default_value) + elif isinstance(default_value, torch.Tensor): + # if tensor, then use the same tensor for all joints + if default_value.shape == (num_envs, num_joints): + param = default_value.float() + else: + raise ValueError( + "Invalid default value tensor shape.\n" + f"Got: {default_value.shape}\n" + f"Expected: {(num_envs, num_joints)}" + ) + else: + raise TypeError( + f"Invalid type for default value: {type(default_value)} for " + + f"actuator on joints {joint_names}. Expected float or Tensor." + ) + else: + raise ValueError("The parameter value is None and no default value is provided.") + + return param + + class ActuatorBase(ABC): """Base class for actuator models over a collection of actuated joints in an articulation. @@ -44,69 +112,27 @@ class ActuatorBase(ABC): """ computed_effort: torch.Tensor - """The computed effort for the actuator group. Shape is (num_envs, num_joints).""" + """The computed effort [N or N·m, depending on joint type] for the actuator group. - applied_effort: torch.Tensor - """The applied effort for the actuator group. Shape is (num_envs, num_joints). - - This is the effort obtained after clipping the :attr:`computed_effort` based on the - actuator characteristics. + Shape is (num_envs, num_joints). """ - effort_limit: torch.Tensor - """The joint effort limit for the actuator group [N or N·m]. Shape is (num_envs, num_joints). - - The actuator's rated force/torque reflected at the joint. It clips explicit-model output and remains - available as the model-facing limit for implicit actuators. When configured separately, it is not - pushed to the physics solver; that is :attr:`effort_limit_sim`. - """ + applied_effort: torch.Tensor + """The applied effort [N or N·m, depending on joint type] for the actuator group. - effort_limit_sim: torch.Tensor - """The solver-level effort clamp for the actuator group [N or N·m]. Shape is (num_envs, num_joints). + Shape is (num_envs, num_joints). - Written to the simulation physics solver and resolved independently of :attr:`effort_limit` when both - fields are configured. + This is the effort obtained after clipping the :attr:`computed_effort` based on the + actuator characteristics. """ - velocity_limit: torch.Tensor - """The joint velocity limit for the actuator group [rad/s or m/s]. Shape is (num_envs, num_joints). + actuator_velocity_limit: torch.Tensor + """The actuator velocity limit [m/s or rad/s, depending on joint type]. Shape is (num_envs, num_joints). The peak velocity of the actuated joint (the actuator's rated speed reflected at the joint, after any gearbox). Feeds the articulation data buffers (e.g. soft joint velocity limits) and explicit-model effort clipping; it is not pushed to the physics solver. Defaults to - :attr:`velocity_limit_sim` when only the solver clamp is configured. - """ - - velocity_limit_sim: torch.Tensor - """The solver-level velocity clamp for the actuator group [rad/s or m/s]. Shape is (num_envs, num_joints). - - Written to the simulation (PhysX ``maxJointVelocity``); resolved independently of - :attr:`velocity_limit`. - """ - - stiffness: torch.Tensor - """The stiffness (P gain) of the PD controller. Shape is (num_envs, num_joints).""" - - damping: torch.Tensor - """The damping (D gain) of the PD controller. Shape is (num_envs, num_joints).""" - - armature: torch.Tensor - """The armature of the actuator joints. Shape is (num_envs, num_joints).""" - - friction: torch.Tensor - """The joint static friction of the actuator joints. Shape is (num_envs, num_joints).""" - - dynamic_friction: torch.Tensor - """The joint dynamic friction of the actuator joints. Shape is (num_envs, num_joints).""" - - viscous_friction: torch.Tensor - """The joint viscous friction of the actuator joints. Shape is (num_envs, num_joints).""" - - _DEFAULT_MAX_EFFORT_SIM: ClassVar[float] = 1.0e9 - """The default maximum effort for the actuator joints in the simulation. Defaults to 1.0e9. - - If the :attr:`ActuatorBaseCfg.effort_limit_sim` is not specified and the actuator is an explicit - actuator, then this value is used. + ``joint_velocity_limit`` when only the solver constraint is configured. """ def __init__( @@ -116,14 +142,10 @@ def __init__( joint_ids: slice | torch.Tensor, num_envs: int, device: str, - stiffness: torch.Tensor | float = 0.0, - damping: torch.Tensor | float = 0.0, - armature: torch.Tensor | float = 0.0, - friction: torch.Tensor | float = 0.0, - dynamic_friction: torch.Tensor | float = 0.0, - viscous_friction: torch.Tensor | float = 0.0, - effort_limit: torch.Tensor | float = torch.inf, - velocity_limit: torch.Tensor | float = torch.inf, + actuator_effort_limit: torch.Tensor | float | None = None, + actuator_velocity_limit: torch.Tensor | float | None = None, + effort_limit: torch.Tensor | float | None = None, # TODO: Deprecated. Remove in 4.0. + velocity_limit: torch.Tensor | float | None = None, # TODO: Deprecated. Remove in 4.0. ): """Initialize the actuator. @@ -131,11 +153,7 @@ def __init__( are not specified in the configuration, then their values provided in the constructor are used. .. note:: - The values in the constructor are typically obtained through the USD values passed from the PhysX API calls - corresponding to the joints in the actuator model; these values serve as default values if the parameters - are not specified in the cfg. - - + The constructor defaults are typically read from the backend's authored joint properties. Args: cfg: The configuration of the actuator model. @@ -144,22 +162,14 @@ def __init__( the joints in the articulation are part of the group. num_envs: Number of articulations in the view. device: Device used for processing. - stiffness: The default joint stiffness (P gain). Defaults to 0.0. - If a tensor, then the shape is (num_envs, num_joints). - damping: The default joint damping (D gain). Defaults to 0.0. - If a tensor, then the shape is (num_envs, num_joints). - armature: The default joint armature. Defaults to 0.0. + actuator_effort_limit: Default actuator-model effort clipping limit + [N or N·m, depending on joint type]. Defaults to infinity. If a tensor, then the shape is (num_envs, num_joints). - friction: The default joint static friction. Defaults to 0.0. - If a tensor, then the shape is (num_envs, num_joints). - dynamic_friction: The default joint dynamic friction. Defaults to 0.0. - If a tensor, then the shape is (num_envs, num_joints). - viscous_friction: The default joint viscous friction. Defaults to 0.0. - If a tensor, then the shape is (num_envs, num_joints). - effort_limit: The default effort limit. Defaults to infinity. - If a tensor, then the shape is (num_envs, num_joints). - velocity_limit: The default velocity limit. Defaults to infinity. + actuator_velocity_limit: Default actuator velocity limit + [m/s or rad/s, depending on joint type]. Defaults to infinity. If a tensor, then the shape is (num_envs, num_joints). + effort_limit: Deprecated alias for :paramref:`actuator_effort_limit`. + velocity_limit: Deprecated alias for :paramref:`actuator_velocity_limit`. """ # save parameters self.cfg = cfg @@ -167,57 +177,62 @@ def __init__( self._device = device self._joint_names = joint_names self._joint_indices = joint_ids - self.joint_property_resolution_table: dict[str, list] = {} - # For explicit models, we do not want to enforce the effort limit through the solver - # (unless it is explicitly set) - if not self.is_implicit_model and self.cfg.effort_limit_sim is None: - self.cfg.effort_limit_sim = self._DEFAULT_MAX_EFFORT_SIM - - # resolve usd, actuator configuration values - # case 1: if usd_value == actuator_cfg_value: all good, - # case 2: if usd_value != actuator_cfg_value: we use actuator_cfg_value - # case 3: if actuator_cfg_value is None: we use usd_value - - to_check = [ - ("velocity_limit_sim", velocity_limit), - ("effort_limit_sim", effort_limit), - ("stiffness", stiffness), - ("damping", damping), - ("armature", armature), - ("friction", friction), - ("dynamic_friction", dynamic_friction), - ("viscous_friction", viscous_friction), - ] - for param_name, usd_val in to_check: - cfg_val = getattr(self.cfg, param_name) - setattr(self, param_name, self._parse_joint_parameter(cfg_val, usd_val)) - new_val = getattr(self, param_name) - - allclose = ( - torch.all(new_val == usd_val) if isinstance(usd_val, (float, int)) else torch.allclose(new_val, usd_val) - ) - if cfg_val is None or not allclose: - self._record_actuator_resolution( - cfg_val=getattr(self.cfg, param_name), - new_val=new_val[0], # new val always has the shape of (num_envs, num_joints) - usd_val=usd_val, - joint_names=joint_names, - joint_ids=joint_ids, - actuator_param=param_name, - ) - - self.velocity_limit = self._parse_joint_parameter(self.cfg.velocity_limit, self.velocity_limit_sim) - # Parse effort_limit with special default handling: - # - If cfg.effort_limit is None, use the original USD value (effort_limit parameter from constructor) - # - Otherwise, use effort_limit_sim as the default - # Please refer to the documentation of the effort_limit and effort_limit_sim parameters for more details. - effort_default = effort_limit if self.cfg.effort_limit is None else self.effort_limit_sim - self.effort_limit = self._parse_joint_parameter(self.cfg.effort_limit, effort_default) # create commands buffers for allocation self.computed_effort = torch.zeros(self._num_envs, self.num_joints, device=self._device) self.applied_effort = torch.zeros_like(self.computed_effort) + # normalize deprecated configuration aliases for direct construction + # TODO: Deprecated. Remove in 4.0. + if ( + self.cfg.effort_limit is not None + or self.cfg.effort_limit_sim is not None + or self.cfg.velocity_limit is not None + or self.cfg.velocity_limit_sim is not None + ): + _resolve_limit_aliases(type(self).__name__, self.cfg, self.joint_names) + + # normalize deprecated constructor aliases + # TODO: Deprecated. Remove in 4.0. + if effort_limit is not None: + warnings.warn( + "The effort_limit constructor argument is deprecated. Use actuator_effort_limit instead; " + "effort_limit will be removed in 4.0.", + DeprecationWarning, + stacklevel=2, + ) + if actuator_effort_limit is not None and not _limits_equal(actuator_effort_limit, effort_limit): + raise ValueError( + "Received conflicting actuator_effort_limit and deprecated effort_limit constructor arguments." + ) + actuator_effort_limit = effort_limit + if velocity_limit is not None: + warnings.warn( + "The velocity_limit constructor argument is deprecated. Use actuator_velocity_limit instead; " + "velocity_limit will be removed in 4.0.", + DeprecationWarning, + stacklevel=2, + ) + if actuator_velocity_limit is not None and not _limits_equal(actuator_velocity_limit, velocity_limit): + raise ValueError( + "Received conflicting actuator_velocity_limit and deprecated velocity_limit constructor arguments." + ) + actuator_velocity_limit = velocity_limit + + # parse the actuator-model limits. Implicit models expose their effort limit as a live + # projection of the articulation joint effort limit instead of a local buffer. + if not self.is_implicit_model: + if actuator_effort_limit is None: + actuator_effort_limit = torch.inf + self.actuator_effort_limit = resolve_joint_parameter( + self.cfg.actuator_effort_limit, actuator_effort_limit, joint_names, num_envs, device + ) + if actuator_velocity_limit is None: + actuator_velocity_limit = torch.inf + self.actuator_velocity_limit = resolve_joint_parameter( + self.cfg.actuator_velocity_limit, actuator_velocity_limit, joint_names, num_envs, device + ) + def __str__(self) -> str: """Returns: A string representation of the actuator group.""" # resolve joint indices for printing @@ -296,85 +311,63 @@ def compute( Helper functions. """ - def _record_actuator_resolution(self, cfg_val, new_val, usd_val, joint_names, joint_ids, actuator_param: str): - if actuator_param not in self.joint_property_resolution_table: - self.joint_property_resolution_table[actuator_param] = [] - table = self.joint_property_resolution_table[actuator_param] - - ids = joint_ids if isinstance(joint_ids, torch.Tensor) else list(range(len(joint_names))) - for idx, name in enumerate(joint_names): - cfg_val_log = "Not Specified" if cfg_val is None else float(new_val[idx]) - default_usd_val = usd_val if isinstance(usd_val, (float, int)) else float(usd_val[0][idx]) - applied_val_log = default_usd_val if cfg_val is None else float(new_val[idx]) - table.append([name, int(ids[idx]), default_usd_val, cfg_val_log, applied_val_log]) - - def _parse_joint_parameter( - self, cfg_value: float | dict[str, float] | None, default_value: float | torch.Tensor | None - ) -> torch.Tensor: - """Parse the joint parameter from the configuration. + def _clip_effort(self, effort: torch.Tensor) -> torch.Tensor: + """Clip the desired torques based on the motor limits. Args: - cfg_value: The parameter value from the configuration. If None, then use the default value. - default_value: The default value to use if the parameter is None. If it is also None, - then an error is raised. + effort: The effort to clip [N or N·m, depending on joint type]. Returns: - The parsed parameter value. - - Raises: - TypeError: If the parameter value is not of the expected type. - TypeError: If the default value is not of the expected type. - ValueError: If the parameter value is None and no default value is provided. - ValueError: If the default value tensor is the wrong shape. + The clipped effort [N or N·m, depending on joint type]. """ - # create parameter buffer - param = torch.zeros(self._num_envs, self.num_joints, device=self._device) - # parse the parameter - if cfg_value is not None: - if isinstance(cfg_value, (float, int)): - # if float, then use the same value for all joints - param[:] = float(cfg_value) - elif isinstance(cfg_value, dict): - # if dict, then parse the regular expression - indices, _, values = string_utils.resolve_matching_names_values(cfg_value, self.joint_names) - # note: need to specify type to be safe (e.g. values are ints, but we want floats) - param[:, indices] = torch.tensor(values, dtype=torch.float, device=self._device) - else: - raise TypeError( - f"Invalid type for parameter value: {type(cfg_value)} for " - + f"actuator on joints {self.joint_names}. Expected float or dict." - ) - elif default_value is not None: - if isinstance(default_value, (float, int)): - # if float, then use the same value for all joints - param[:] = float(default_value) - elif isinstance(default_value, torch.Tensor): - # if tensor, then use the same tensor for all joints - if default_value.shape == (self._num_envs, self.num_joints): - param = default_value.float() - else: - raise ValueError( - "Invalid default value tensor shape.\n" - f"Got: {default_value.shape}\n" - f"Expected: {(self._num_envs, self.num_joints)}" - ) - else: - raise TypeError( - f"Invalid type for default value: {type(default_value)} for " - + f"actuator on joints {self.joint_names}. Expected float or Tensor." - ) - else: - raise ValueError("The parameter value is None and no default value is provided.") + return torch.clip(effort, min=-self.actuator_effort_limit, max=self.actuator_effort_limit) - return param + @property + def effort_limit(self) -> torch.Tensor: + """Deprecated actuator effort limit [N or N·m, depending on joint type]. - def _clip_effort(self, effort: torch.Tensor) -> torch.Tensor: - """Clip the desired torques based on the motor limits. + .. deprecated:: 3.0 + Use :attr:`actuator_effort_limit` instead. This alias will be removed in 4.0. + """ + warnings.warn( + "ActuatorBase.effort_limit is deprecated. Use actuator_effort_limit instead; " + "effort_limit will be removed in 4.0.", + DeprecationWarning, + stacklevel=2, + ) + return self.actuator_effort_limit + + @effort_limit.setter + def effort_limit(self, value: torch.Tensor) -> None: + warnings.warn( + "ActuatorBase.effort_limit is deprecated. Use actuator_effort_limit instead; " + "effort_limit will be removed in 4.0.", + DeprecationWarning, + stacklevel=2, + ) + self.actuator_effort_limit = value - Args: - desired_torques: The desired torques to clip. + @property + def velocity_limit(self) -> torch.Tensor: + """Deprecated actuator velocity limit [m/s or rad/s, depending on joint type]. - Returns: - The clipped torques. + .. deprecated:: 3.0 + Use :attr:`actuator_velocity_limit` instead. This alias will be removed in 4.0. """ - return torch.clip(effort, min=-self.effort_limit, max=self.effort_limit) + warnings.warn( + "ActuatorBase.velocity_limit is deprecated. Use actuator_velocity_limit instead; " + "velocity_limit will be removed in 4.0.", + DeprecationWarning, + stacklevel=2, + ) + return self.actuator_velocity_limit + + @velocity_limit.setter + def velocity_limit(self, value: torch.Tensor) -> None: + warnings.warn( + "ActuatorBase.velocity_limit is deprecated. Use actuator_velocity_limit instead; " + "velocity_limit will be removed in 4.0.", + DeprecationWarning, + stacklevel=2, + ) + self.actuator_velocity_limit = value diff --git a/source/isaaclab/isaaclab/actuators/actuator_base_cfg.py b/source/isaaclab/isaaclab/actuators/actuator_base_cfg.py index 24b3de5d4d26..756bfe79faa0 100644 --- a/source/isaaclab/isaaclab/actuators/actuator_base_cfg.py +++ b/source/isaaclab/isaaclab/actuators/actuator_base_cfg.py @@ -10,6 +10,15 @@ from isaaclab.utils.configclass import configclass +def _is_implicit_actuator_cfg(cfg: ActuatorBaseCfg) -> bool: + """Return whether an actuator configuration resolves to an implicit actuator class. + + Reads the :attr:`~isaaclab.actuators.ActuatorBase.is_implicit_model` class flag. + Lazily resolving string references participate through attribute forwarding. + """ + return bool(getattr(cfg.class_type, "is_implicit_model", False)) + + @configclass class ActuatorBaseCfg: """Configuration for default actuators in an articulation.""" @@ -27,23 +36,20 @@ class ActuatorBaseCfg: This can be a list of joint names or a list of regex expressions (e.g. ".*"). """ - effort_limit: dict[str, float] | float | None = None - """Force/Torque limit of the joints in the group. Defaults to None. - - This is the actuator's rated force/torque reflected at the joint. It clips the output of explicit - actuator models and remains available as the model-facing limit for implicit actuators. If None, it - uses the value specified in the USD joint prim. An implicit actuator configured with only - :attr:`effort_limit_sim` also uses that solver clamp as its model-facing limit. + actuator_effort_limit: dict[str, float] | float | None = None + """Actuator-model effort clipping limit [N or N·m, depending on joint type]. - .. attention:: - - Use :attr:`effort_limit_sim` for the solver-level clamp. Implicit actuators resolve the two - fields independently when both are configured. When only one is configured, it fills both fields - for backwards compatibility. + The actuator's rated force/torque reflected at the joint. Explicit actuator models + clip their computed effort with it; implicit actuators use it as the model-facing + limit for effort telemetry. If None, it defaults to the authored/USD joint effort + limit (explicit) or tracks the live solver limit (implicit). It is not a solver + limit; that is :attr:`joint_effort_limit`. + :class:`~isaaclab.actuators.RemotizedPDActuator` instead uses the + angle-dependent limits in its ``joint_parameter_lookup``. """ - velocity_limit: dict[str, float] | float | None = None + actuator_velocity_limit: dict[str, float] | float | None = None """Velocity limit of the joints in the group. Defaults to None. This limit is used by the actuator model. If None, the limit is set to the value specified @@ -51,52 +57,44 @@ class ActuatorBaseCfg: .. attention:: - This attribute describes the joint's peak velocity, i.e. the actuator's rated speed - reflected at the joint (after any gearbox). It populates the articulation data + This attribute describes the actuator's peak velocity, i.e. the actuator's rated speed + reflected at the joint (after any gearbox). It populates the actuator data buffers (e.g. :attr:`~isaaclab.assets.ArticulationData.soft_joint_vel_limits`, read by - velocity-limit terminations and rewards) and clips the effort output of explicit - actuator models, but it is **not** pushed to the physics solver. - - Use :attr:`velocity_limit_sim` to additionally impose a solver-level hard clamp - (PhysX ``maxJointVelocity``). A physical actuator limits joint speed through its - torque curve rather than a kinematic clamp, so the two limits are resolved - independently. When only :attr:`velocity_limit_sim` is set, it also serves as the - joint velocity limit. + velocity-limit terminations and rewards). Explicit models with speed-dependent limits, + such as :class:`DCMotor`, also use it to clip effort. It is **not** pushed to the physics + solver. + + Use :attr:`joint_velocity_limit` to request a solver-level hard clamp. A physical + actuator limits joint speed through its torque curve rather than a kinematic clamp, + so the two limits are resolved independently. When only + :attr:`joint_velocity_limit` is set, it also serves as the joint velocity limit. """ - effort_limit_sim: dict[str, float] | float | None = None - """Solver-level effort clamp of the joints in the group. Defaults to None. + joint_effort_limit: dict[str, float] | float | None = None + """Construction-time joint solver effort override [N or N·m, depending on joint type]. - The effort limit is used to constrain the computed joint efforts in the physics engine. If the - computed effort exceeds this limit, the physics engine will clip the effort to this value. It is - resolved independently of :attr:`effort_limit` when both fields are configured. + The live value is owned by :class:`isaaclab.assets.ArticulationData`. + """ - Since explicit actuators (e.g. DC motor), compute and clip the effort in the actuator model, this - limit is by default set to a large value to prevent the physics engine from any additional clipping. - However, at times, it may be necessary to set this limit to a smaller value as a safety measure. + joint_velocity_limit: dict[str, float] | float | None = None + """Construction-time requested joint solver velocity limit [m/s or rad/s, depending on joint type]. - If None, the limit is resolved based on the type of actuator model: + The live value is owned by :class:`isaaclab.assets.ArticulationData`; enforcement is + backend-dependent. + """ - * For implicit actuators, the limit is set to :attr:`effort_limit` when it is configured, otherwise - to the value specified in the USD joint prim. - * For explicit actuators, the limit is set to 1.0e9. + effort_limit_sim: dict[str, float] | float | None = None + """Deprecated alias for :attr:`joint_effort_limit`. + .. deprecated:: 3.0 + Use :attr:`joint_effort_limit` instead. This alias will be removed in 4.0. """ velocity_limit_sim: dict[str, float] | float | None = None - """Velocity limit of the joints in the group applied to the simulation physics solver. Defaults to None. - - The velocity limit is used to constrain the joint velocities in the physics engine. The joint will only - be able to reach this velocity if the joint's effort limit is sufficiently large. If the joint is moving - faster than this velocity, the physics engine will actually try to brake the joint to reach this velocity. - - If None, the limit is set to the value specified in the USD joint prim for both implicit and explicit actuators. - - .. tip:: - If the velocity limit is too tight, the physics engine may have trouble converging to a solution. - In such cases, we recommend either keeping this value sufficiently large or tuning the stiffness and - damping parameters of the joint to ensure the limits are not violated. + """Deprecated alias for :attr:`joint_velocity_limit`. + .. deprecated:: 3.0 + Use :attr:`joint_velocity_limit` instead. This alias will be removed in 4.0. """ stiffness: dict[str, float] | float | None = MISSING @@ -158,3 +156,20 @@ class ActuatorBaseCfg: viscous_friction: dict[str, float] | float | None = None """The viscous friction coefficient of the joints in the group. Defaults to None. """ + + effort_limit: dict[str, float] | float | None = None + """Deprecated effort limit [N or N·m, depending on joint type]. + + .. deprecated:: 3.0 + For explicit actuators, use :attr:`actuator_effort_limit`. For implicit + actuators, use :attr:`joint_effort_limit`. This alias will be removed in 4.0. + """ + + velocity_limit: dict[str, float] | float | None = None + """Deprecated velocity limit [m/s or rad/s, depending on joint type]. + + .. deprecated:: 3.0 + Use :attr:`actuator_velocity_limit` for the actuator-model limit or + :attr:`joint_velocity_limit` for the solver limit. This alias will be + removed in 4.0. + """ diff --git a/source/isaaclab/isaaclab/actuators/actuator_collection.py b/source/isaaclab/isaaclab/actuators/actuator_collection.py new file mode 100644 index 000000000000..ddf8e213a189 --- /dev/null +++ b/source/isaaclab/isaaclab/actuators/actuator_collection.py @@ -0,0 +1,987 @@ +# 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 + +"""Runtime actuator collection for articulations.""" + +from __future__ import annotations + +import copy +import itertools +import logging +import warnings +from collections.abc import Iterator, Mapping, Sequence + +import torch +import warp as wp +from prettytable import PrettyTable + +from isaaclab.utils.types import ArticulationActions +from isaaclab.utils.warp import ProxyArray +from isaaclab.utils.warp.launch_cache import _WarpLaunchCache + +from . import actuator_kernels +from ._compat import _resolve_limit_aliases +from .actuator_base import ActuatorBase, resolve_joint_parameter +from .actuator_base_cfg import ActuatorBaseCfg, _is_implicit_actuator_cfg +from .actuator_control import ActuatorControl +from .actuator_pd import IdealPDActuator, ImplicitActuator + +logger = logging.getLogger(__name__) + + +class ActuatorCollection(Mapping[str, "ActuatorBase | object"]): + """Read-only runtime collection of actuator groups for one articulation. + + Mapping entries return whoever owns the group. Isaac Lab-executed groups map to + their :class:`~isaaclab.actuators.ActuatorBase` model instances. Newton-executed + groups map to the Newton :class:`~newton.actuators.Actuator` objects that drive + their joints, so users read and modify the owning controller directly. Newton + merges structurally identical joints into one actuator, so several groups can + map to the same object (or to a tuple when a group spans several); the + collection keeps each group's joint indices, which + :func:`~isaaclab.actuators.newton.read_group_parameter` and + :func:`~isaaclab.actuators.newton.write_group_parameter` use for + group-scoped, user-ordered access. + + Configure membership through :attr:`isaaclab.assets.ArticulationCfg.actuators` + before construction; assigning or deleting mapping entries raises + :class:`TypeError`. Each joint can belong to at most one group; overlapping + joint selections raise :class:`ValueError` during construction. + + Plain :class:`~isaaclab.actuators.ImplicitActuator` groups are not executed one + group at a time: a single internal executor computes all of their joints in one + fused kernel launch. All other Lab-executed groups, including subclasses of + :class:`~isaaclab.actuators.ImplicitActuator`, execute per group. + """ + + # Initialization. + + def __init__( + self, + actuator_cfgs: dict[str, ActuatorBaseCfg], + control: ActuatorControl, + *, + debug_value_resolution: bool = False, + ): + """Initialize the actuator collection. + + Args: + actuator_cfgs: Mapping of actuator group names to actuator configs. + control: Backend control bridge for state reads and sim writes. + debug_value_resolution: Whether to log actuator value resolution. + """ + self._control = control + self._groups: dict[str, ActuatorBase | object] = {} + self._group_joint_names: dict[str, list[str]] = {} + self._group_joint_indices: dict[str, slice | torch.Tensor] = {} + self._implicit_group_names: set[str] = set() + self._native_group_names: set[str] = set() + self._debug_value_resolution = debug_value_resolution + self._joint_property_resolution_rows: dict[str, dict[str, tuple[tuple[object, ...], ...]]] = {} + self._has_implicit_actuators = False + self._launch_cache = _WarpLaunchCache(self.device) + + resolved_cfgs = {name: cfg.copy() for name, cfg in actuator_cfgs.items()} + resolved_group_joints = self._resolve_group_joints(resolved_cfgs) + self._allocate_buffers() + self._target_command = ActuatorTargetCommand(self) + self._output_command = ActuatorOutputCommand(self) + for name, cfg in resolved_cfgs.items(): + _resolve_limit_aliases(name, cfg, resolved_group_joints[name][1]) + self._native_group_names = self._control.prepare_native_actuators(self, resolved_cfgs) + self._build_groups(resolved_cfgs, resolved_group_joints) + self._newton_selection = self._control.finalize_native_actuators(self) + if self._native_group_names: + if self._newton_selection is None: + raise RuntimeError( + "The backend declared Newton-executed actuator groups " + f"{sorted(self._native_group_names)} but finalize_native_actuators returned no selection." + ) + for actuator_name in self._native_group_names: + self._groups[actuator_name] = self._resolve_newton_group_actuators(actuator_name) + self._validate_coverage() + self._build_execution_plan() + if self._debug_value_resolution: + self._print_value_resolution_table() + if not self._control.native_actuator_path_active: + explicit_group_names = [ + name for name, actuator in self._groups.items() if isinstance(actuator, IdealPDActuator) + ] + if explicit_group_names: + warnings.warn( + "Isaac Lab execution of explicit actuator models is deprecated. Use Newton actuator execution " + f"instead. Affected groups: {', '.join(explicit_group_names)}.", + DeprecationWarning, + stacklevel=2, + ) + + # Public interface. + + def __getitem__(self, name: str) -> ActuatorBase | object: + return self._groups[name] + + def __iter__(self) -> Iterator[str]: + return iter(self._groups) + + def __len__(self) -> int: + return len(self._groups) + + def __setitem__(self, name: str, actuator: ActuatorBase) -> None: + raise TypeError("ActuatorCollection membership is fixed after initialization.") + + def __delitem__(self, name: str) -> None: + raise TypeError("ActuatorCollection membership is fixed after initialization.") + + @property + def target_command(self) -> ActuatorTargetCommand: + """Commands received by the actuator models.""" + return self._target_command + + @property + def output_command(self) -> ActuatorOutputCommand: + """Processed commands produced for the simulated joints. + + This view is not submitted-command telemetry for native controllers, which + bypass the processed-command arrays. + """ + return self._output_command + + @property + def num_instances(self) -> int: + """Number of articulation instances.""" + return self._control.num_instances + + @property + def num_joints(self) -> int: + """Number of articulation joints.""" + return self._control.num_joints + + @property + def device(self) -> str: + """Warp/Torch device string.""" + return self._control.device + + @property + def has_implicit_actuators(self) -> bool: + """Whether any configured actuator group is implicit.""" + return self._has_implicit_actuators + + @property + def computed_effort(self) -> ProxyArray: + """Joint efforts computed before clipping [N or N·m, depending on joint type].""" + return self._computed_effort_ta + + @property + def applied_effort(self) -> ProxyArray: + """Joint efforts applied after clipping [N or N·m, depending on joint type].""" + return self._applied_effort_ta + + # Lifecycle. + + def reset(self, env_ids: Sequence[int] | slice | None = None) -> None: + """Reset all actuator group states. + + Args: + env_ids: Environment indices to reset. Defaults to all environments. + """ + group_env_ids = self._control._normalize_index_sequence(env_ids) + for actuator in self._groups.values(): + # Newton-executed groups are reset through the backend below. + if isinstance(actuator, ActuatorBase): + actuator.reset(group_env_ids) + self._control.reset_native_actuators(slice(None) if group_env_ids is None else group_env_ids) + + def compute(self, dt: float = 0.0) -> None: + """Compute processed actuator commands and telemetry. + + Args: + dt: Physics step size [s]. + """ + if self._control.compute_native_actuators(self, dt): + return + + if self._implicit_executor is not None: + self._implicit_executor.launch(self) + joint_pos = self._control.joint_pos + joint_vel = self._control.joint_vel + for actuator, joint_indices_wp in self._execution_actuators: + joint_indices = actuator.joint_indices + control_action = ArticulationActions( + joint_positions=self.target_command.position.torch[:, joint_indices], + joint_velocities=self.target_command.velocity.torch[:, joint_indices], + joint_efforts=self.target_command.effort.torch[:, joint_indices], + joint_indices=joint_indices, + ) + control_action = actuator.compute( + control_action, + joint_pos=joint_pos.torch[:, joint_indices], + joint_vel=joint_vel.torch[:, joint_indices], + ) + self._scatter_actuator_output(actuator, control_action, joint_indices_wp) + + def submit_commands(self) -> None: + """Submit processed actuator command buffers through the backend control object.""" + self._control.submit_commands(self) + + def _newton_group_columns(self, name: str) -> torch.Tensor: + """Backend view columns of one group's joints, in group joint order.""" + joint_ids = self._group_joint_indices[name] + if isinstance(joint_ids, slice): + columns = torch.arange(self.num_joints, device=self.device) + else: + columns = joint_ids.to(self.device, dtype=torch.long) + user_to_backend = self._newton_selection.joint_user_to_backend_indices + if user_to_backend is not None: + columns = torch.tensor(user_to_backend, dtype=torch.long, device=self.device)[columns] + return columns + + # Construction and property resolution. + + def _allocate_buffers(self) -> None: + """Allocate articulation-wide command and telemetry buffers.""" + shape = (self.num_instances, self.num_joints) + # Staging buffers for the actuators I/O + self._joint_pos_target = wp.zeros(shape, dtype=wp.float32, device=self.device) + self._joint_vel_target = wp.zeros(shape, dtype=wp.float32, device=self.device) + self._joint_effort_target = wp.zeros(shape, dtype=wp.float32, device=self.device) + self._joint_pos_target_sim = wp.zeros(shape, dtype=wp.float32, device=self.device) + self._joint_vel_target_sim = wp.zeros(shape, dtype=wp.float32, device=self.device) + self._joint_effort_target_sim = wp.zeros(shape, dtype=wp.float32, device=self.device) + # Telemetry buffers + self._computed_effort = wp.zeros(shape, dtype=wp.float32, device=self.device) + self._applied_effort = wp.zeros(shape, dtype=wp.float32, device=self.device) + self._soft_joint_vel_limits = wp.zeros(shape, dtype=wp.float32, device=self.device) + # All joint IDs and masks + self._all_joint_ids = wp.array(list(range(self.num_joints)), dtype=wp.int32, device=self.device) + self._all_true_env_mask = wp.ones(self.num_instances, dtype=wp.bool, device=self.device) + self._all_true_joint_mask = wp.ones(self.num_joints, dtype=wp.bool, device=self.device) + + # Proxy arrays for the buffers + self._joint_pos_target_ta = ProxyArray(self._joint_pos_target) + self._joint_vel_target_ta = ProxyArray(self._joint_vel_target) + self._joint_effort_target_ta = ProxyArray(self._joint_effort_target) + self._joint_pos_target_sim_ta = ProxyArray(self._joint_pos_target_sim) + self._joint_vel_target_sim_ta = ProxyArray(self._joint_vel_target_sim) + self._joint_effort_target_sim_ta = ProxyArray(self._joint_effort_target_sim) + self._computed_effort_ta = ProxyArray(self._computed_effort) + self._applied_effort_ta = ProxyArray(self._applied_effort) + + def _resolve_group_joints( + self, actuator_cfgs: dict[str, ActuatorBaseCfg] + ) -> dict[str, tuple[ProxyArray, list[str]]]: + """Resolve group selectors and reject joints assigned to multiple groups.""" + resolved: dict[str, tuple[ProxyArray, list[str]]] = {} + joint_owners: dict[str, str] = {} + # Resolve exact joint names and indices + for actuator_name, actuator_cfg in actuator_cfgs.items(): + joint_ids, joint_names = self._control.find_joints(actuator_cfg.joint_names_expr) + if len(joint_names) == 0: + raise ValueError( + f"No joints found for actuator group: {actuator_name} with joint name expression:" + f" {actuator_cfg.joint_names_expr}." + ) + # Check for duplicate joint names + for joint_name in joint_names: + owner = joint_owners.get(joint_name) + if owner is not None and owner != actuator_name: + raise ValueError( + f"Joint '{joint_name}' is assigned to multiple actuator groups: '{owner}' and" + f" '{actuator_name}'." + ) + joint_owners[joint_name] = actuator_name + resolved[actuator_name] = (joint_ids, joint_names) + return resolved + + def _build_groups( + self, + actuator_cfgs: dict[str, ActuatorBaseCfg], + resolved_group_joints: dict[str, tuple[list[int] | ProxyArray, list[str]]], + ) -> None: + """Construct actuator groups and apply their resolved joint properties. + + Newton-executed groups never instantiate an Isaac Lab actuator model: the + Newton controllers own their parameters, so a Lab model would only hold + misleading construction-time snapshots. Their mapping entries are filled + with the owning Newton actuator objects after backend finalization. + """ + construction_records: list[tuple[dict[str, torch.Tensor], torch.Tensor | slice, bool, bool]] = [] + for actuator_name, actuator_cfg in actuator_cfgs.items(): + joint_ids, joint_names = resolved_group_joints[actuator_name] + if len(joint_names) == self.num_joints: + actuator_joint_ids: slice | torch.Tensor = slice(None) + elif isinstance(joint_ids, ProxyArray): + actuator_joint_ids = joint_ids.torch + else: + actuator_joint_ids = torch.tensor(joint_ids, device=self.device, dtype=torch.int32) + self._group_joint_names[actuator_name] = joint_names + self._group_joint_indices[actuator_name] = actuator_joint_ids + + joint_defaults = self._control.get_default_joint_properties(actuator_joint_ids) + implicit = _is_implicit_actuator_cfg(actuator_cfg) + self._has_implicit_actuators = self._has_implicit_actuators or implicit + if implicit: + self._implicit_group_names.add(actuator_name) + native_managed = actuator_name in self._native_group_names + properties, table_rows = self._resolve_joint_properties( + actuator_cfg, + joint_defaults, + joint_names, + actuator_joint_ids, + ) + if native_managed: + # placeholder keeps configuration order; replaced by the Newton actuator + # objects once the backend selection is finalized. + self._groups[actuator_name] = None + else: + actuator_kwargs = dict( + cfg=actuator_cfg, + joint_names=joint_names, + joint_ids=actuator_joint_ids, + num_envs=self.num_instances, + device=self.device, + stiffness=properties["stiffness"], + damping=properties["damping"], + actuator_velocity_limit=properties["joint_velocity_limit"], + ) + if implicit: + # implicit groups read the solver limit live after binding; the resolved value + # seeds the pre-binding construction buffer. + actuator_kwargs["joint_effort_limit"] = properties["joint_effort_limit"] + else: + # explicit models default their clip limit to the authored joint effort limit. + actuator_kwargs["actuator_effort_limit"] = joint_defaults["joint_effort_limit"] + self._groups[actuator_name] = actuator_cfg.class_type(**actuator_kwargs) + if self._debug_value_resolution: + self._joint_property_resolution_rows[actuator_name] = table_rows + construction_records.append( + ( + properties, + actuator_joint_ids, + implicit, + native_managed, + ) + ) + + for properties, joint_ids, implicit, native_managed in construction_records: + self._control.write_resolved_joint_properties( + properties, + joint_ids, + implicit=implicit, + native_managed=native_managed, + ) + for actuator in self._groups.values(): + if isinstance(actuator, ImplicitActuator): + actuator._bind_actuator_parameters(self._control) + + def _resolve_newton_group_actuators(self, name: str) -> object: + """Return the Newton actuator object(s) that drive one group's joints. + + Newton merges structurally identical joints into one actuator, so the + returned object can be shared between groups. A single covering actuator + is returned directly; a group spanning several returns them as a tuple. + """ + view = self._newton_selection.view + columns = self._newton_group_columns(name) + matched = [] + for actuator in self._newton_selection.actuators: + mapping = wp.to_torch(view._get_actuator_dof_mapping(actuator)) + env_columns = mapping.reshape(self.num_instances, -1)[0] + if bool((env_columns[columns] >= 0).any()): + matched.append(actuator) + if not matched: + raise RuntimeError(f"No Newton actuator drives any joint of group '{name}'.") + return matched[0] if len(matched) == 1 else tuple(matched) + + def _implicit_group_joint_indices(self) -> list[slice | torch.Tensor]: + """Joint selectors of the implicit groups, consumed by backend implicit-DOF masks.""" + return [self._group_joint_indices[name] for name in self._implicit_group_names] + + def _resolve_joint_properties( + self, + cfg: ActuatorBaseCfg, + defaults: dict[str, torch.Tensor], + joint_names: list[str], + joint_ids: torch.Tensor | slice, + ) -> tuple[dict[str, torch.Tensor], dict[str, tuple[tuple[object, ...], ...]]]: + """Resolve fresh construction-only joint properties for one actuator group. + + The solver keeps the authored joint limits unless the configuration overrides + them; explicit actuator models no longer widen the solver effort limit. + """ + values: dict[str, torch.Tensor] = {} + resolution_rows: dict[str, tuple[tuple[object, ...], ...]] = {} + for cfg_name in ( + "stiffness", + "damping", + "armature", + "friction", + "dynamic_friction", + "viscous_friction", + "joint_effort_limit", + "joint_velocity_limit", + ): + default_value = defaults[cfg_name] + cfg_value = getattr(cfg, cfg_name) + value = self._resolve_joint_property(cfg_value, default_value, joint_names) + values[cfg_name] = value + if self._debug_value_resolution: + rows = self._joint_property_resolution_rows_for( + cfg_value, + value, + default_value, + joint_names, + joint_ids, + ) + if rows: + resolution_rows[cfg_name] = rows + + return values, resolution_rows + + def _resolve_joint_property( + self, + cfg_value: float | dict[str, float] | None, + default_value: torch.Tensor, + joint_names: list[str], + ) -> torch.Tensor: + """Resolve one group-shaped joint property from config and authored defaults.""" + return resolve_joint_parameter(cfg_value, default_value, joint_names, self.num_instances, self.device) + + def _joint_property_resolution_rows_for( + self, + cfg_value: float | dict[str, float] | None, + value: torch.Tensor, + default_value: torch.Tensor, + joint_names: list[str], + joint_ids: torch.Tensor | slice, + ) -> tuple[tuple[object, ...], ...]: + """Formats joint property resolution rows for debugging. (Actuators property table output.)""" + if cfg_value is not None and torch.allclose(value, default_value): + return () + if isinstance(joint_ids, slice): + ids = range(self.num_joints) + else: + ids = tuple(int(joint_id) for joint_id in joint_ids.tolist()) + return tuple( + ( + name, + ids[index], + float(default_value[0, index]), + "Not Specified" if cfg_value is None else float(value[0, index]), + float(default_value[0, index]) if cfg_value is None else float(value[0, index]), + ) + for index, name in enumerate(joint_names) + ) + + # Execution planning and runtime. + + def _joint_indices_as_wp(self, actuator: ActuatorBase) -> wp.array(dtype=wp.int32): + """Return an actuator group's joint indices as a Warp int32 array.""" + if actuator.joint_indices == slice(None) or actuator.joint_indices is None: + return self._all_joint_ids + joint_indices = actuator.joint_indices + if isinstance(joint_indices, wp.array): + return joint_indices + return wp.from_torch(joint_indices.to(self.device, dtype=torch.int32).contiguous(), dtype=wp.int32) + + def _joint_indices_as_torch(self, actuator: ActuatorBase) -> torch.Tensor: + """Return an actuator group's joint indices as a contiguous Torch int32 tensor.""" + if actuator.joint_indices == slice(None) or actuator.joint_indices is None: + return torch.arange(self.num_joints, dtype=torch.int32, device=self.device) + joint_indices = actuator.joint_indices + if isinstance(joint_indices, wp.array): + joint_indices = wp.to_torch(joint_indices) + return joint_indices.to(self.device, dtype=torch.int32).contiguous() + + def _build_execution_plan(self) -> None: + """Partition the actuator groups into the fused implicit executor and a per-group list. + + Plain :class:`~isaaclab.actuators.ImplicitActuator` groups only produce effort + telemetry, so they are not executed one group at a time: their joint indices are + aggregated while parsing and one executor computes all of them in a single fused + kernel launch. Subclasses may override :meth:`~ActuatorBase.compute`, so they stay + in the per-group list along with the explicit models. On a native actuator path the + backend replaces :meth:`compute` entirely and the plan is left empty. + """ + self._execution_actuators: list[tuple[ActuatorBase, wp.array(dtype=wp.int32)]] = [] + implicit_names: list[str] = [] + implicit_groups: list[ImplicitActuator] = [] + if self._control.native_actuator_path_active: + self._implicit_executor = None + return + for name, group in self._groups.items(): + if name in self._native_group_names: + continue + if type(group) is ImplicitActuator: + implicit_names.append(name) + implicit_groups.append(group) + else: + self._execution_actuators.append((group, self._joint_indices_as_wp(group))) + self._implicit_executor = ( + _ImplicitExecutor(self, tuple(implicit_names), tuple(implicit_groups)) if implicit_groups else None + ) + + def _rebind_state_inputs(self) -> None: + """Rebind the implicit executor after backend state storage is replaced. + + Per-group execution reads backend state through the control object on every + :meth:`compute` call, so only the cached implicit launch holds state references + that need rebinding. + """ + if self._implicit_executor is not None: + self._implicit_executor.rebind(self) + + def _scatter_actuator_output( + self, + actuator: ActuatorBase, + control_action: ArticulationActions, + joint_indices: wp.array(dtype=wp.int32) | None = None, + ) -> None: + """Publish one explicit actuator's processed commands and telemetry.""" + if joint_indices is None: + joint_indices = self._joint_indices_as_wp(actuator) + inputs = [ + control_action.joint_positions, + control_action.joint_velocities, + control_action.joint_efforts, + actuator.computed_effort, + actuator.applied_effort, + actuator.actuator_velocity_limit, + joint_indices, + ] + outputs = [ + self._joint_pos_target_sim, + self._joint_vel_target_sim, + self._joint_effort_target_sim, + self._computed_effort, + self._applied_effort, + self._soft_joint_vel_limits, + ] + wp.launch( + actuator_kernels.scatter_explicit_actuator_outputs, + dim=(self.num_instances, joint_indices.shape[0]), + inputs=inputs, + outputs=outputs, + device=self.device, + ) + + # Diagnostics. + + def _validate_coverage(self) -> None: + """Warn when actuator groups do not cover the expected movable joints.""" + if self.num_joints == 0: + return + total_act_joints = sum(len(joint_names) for joint_names in self._group_joint_names.values()) + expected_joints = self.num_joints - self._control.num_fixed_tendons + if total_act_joints != expected_joints: + logger.warning( + "Actuator groups cover %s joints; expected %s after accounting for fixed tendons.", + total_act_joints, + expected_joints, + ) + + def _print_value_resolution_table(self) -> None: + """Log construction-time differences between authored and configured values.""" + table = PrettyTable(["Group", "Property", "Name", "ID", "USD Value", "ActuatorCfg Value", "Applied"]) + for actuator_group in self._groups: + group_count = 0 + for property_name, resolution_details in self._joint_property_resolution_rows[actuator_group].items(): + for prop_idx, resolution_detail in enumerate(resolution_details): + actuator_group_str = actuator_group if group_count == 0 else "" + property_str = property_name if prop_idx == 0 else "" + fmt = [f"{value:.2e}" if isinstance(value, float) else str(value) for value in resolution_detail] + table.add_row([actuator_group_str, property_str, *fmt]) + group_count += 1 + logger.warning("\nActuatorCfg-USD Value Discrepancy Resolution (matching values are skipped): \n%s", table) + + +class _ImplicitExecutor: + """Fused executor for the collection's plain :class:`ImplicitActuator` groups. + + A single group executes as itself. Multiple groups execute through one private + shadow actuator covering the union of their joint indices, so the per-step cost + is one kernel launch regardless of how the configuration partitions the joints. + The logical groups' telemetry tensors are re-pointed at contiguous views of the + shadow's buffers, so per-group reads observe the fused results directly. + """ + + _cache_key_counter = itertools.count() + """Monotonic launch-cache key source; ``id()`` keys could be reused after garbage collection.""" + + def __init__(self, collection: ActuatorCollection, names: tuple[str, ...], groups: tuple[ImplicitActuator, ...]): + self._cache_key = ("implicit", next(_ImplicitExecutor._cache_key_counter)) + self.group_names = names + if len(groups) == 1: + self.actuator = groups[0] + joint_indices = collection._joint_indices_as_torch(groups[0]) + else: + joint_indices = torch.cat([collection._joint_indices_as_torch(group) for group in groups]) + self.actuator = self._build_shadow_actuator(groups, joint_indices) + self.joint_indices_wp = wp.from_torch(joint_indices, dtype=wp.int32) + self.kernel_inputs: list[wp.array] | None = None + self.kernel_outputs: list[wp.array] | None = None + self._assemble_kernel_arrays(collection) + + @staticmethod + def _build_shadow_actuator(groups: tuple[ImplicitActuator, ...], joint_indices: torch.Tensor) -> ImplicitActuator: + """Build one private shadow actuator covering all the logical groups' joints. + + Retains the first group's config metadata; replaces the execution tensors below + without cloning the logical groups' tensor storage. The groups' telemetry tensors + become contiguous views of the shadow's buffers. + """ + shadow = copy.copy(groups[0]) + shadow._joint_names = [name for group in groups for name in group.joint_names] + shadow._joint_indices = joint_indices + shadow.actuator_velocity_limit = torch.cat([group.actuator_velocity_limit for group in groups], dim=1) + shadow.computed_effort = torch.zeros(shadow._num_envs, len(shadow._joint_names), device=shadow._device) + shadow.applied_effort = torch.zeros_like(shadow.computed_effort) + start = 0 + for group in groups: + group_slice = slice(start, start + group.num_joints) + start += group.num_joints + group.computed_effort = shadow.computed_effort[:, group_slice] + group.applied_effort = shadow.applied_effort[:, group_slice] + return shadow + + def _assemble_kernel_arrays(self, collection: ActuatorCollection) -> None: + """Assemble the implicit kernel argument arrays. + + Existing argument lists are updated in place so holders of the list objects + observe rebound backend state. + """ + control = collection._control + inputs = [ + collection._joint_pos_target, + collection._joint_vel_target, + collection._joint_effort_target, + control.joint_pos.warp, + control.joint_vel.warp, + control.joint_stiffness.warp, + control.joint_damping.warp, + control.joint_effort_limits.warp, + wp.from_torch(self.actuator.actuator_velocity_limit, dtype=wp.float32), + self.joint_indices_wp, + ] + outputs = [ + wp.from_torch(self.actuator.computed_effort, dtype=wp.float32), + wp.from_torch(self.actuator.applied_effort, dtype=wp.float32), + collection._joint_pos_target_sim, + collection._joint_vel_target_sim, + collection._joint_effort_target_sim, + collection._computed_effort, + collection._applied_effort, + collection._soft_joint_vel_limits, + ] + if self.kernel_inputs is None: + self.kernel_inputs = inputs + self.kernel_outputs = outputs + else: + self.kernel_inputs[:] = inputs + self.kernel_outputs[:] = outputs + + def launch(self, collection: ActuatorCollection) -> None: + """Compute all the executor's joints through the cached Warp launch.""" + collection._launch_cache.launch( + self._cache_key, + actuator_kernels.compute_implicit_actuator_batch, + dim=(collection.num_instances, self.joint_indices_wp.shape[0]), + inputs=self.kernel_inputs, + outputs=self.kernel_outputs, + ) + + def rebind(self, collection: ActuatorCollection) -> None: + """Reassemble the kernel arguments after backend state storage is replaced.""" + self._assemble_kernel_arrays(collection) + collection._launch_cache.clear(self._cache_key) + + +class ActuatorTargetCommand: + """Commands received by the actuator models. + + Position and velocity commands use joint-side coordinates. All command + arrays are indexed by articulation joint, not by motor shaft. + + Index selectors must contain unique environment and joint indices. Repeated + indices dispatch concurrent writes to the same destination and produce an + undefined result. Deduplicate selectors or use mask setters. + """ + + def __init__(self, collection: ActuatorCollection) -> None: + """Initialize the command view. + + Args: + collection: Owning actuator collection. + """ + self._collection = collection + + @property + def position(self) -> ProxyArray: + """Desired positions [m or rad, depending on joint type].""" + return self._collection._joint_pos_target_ta + + @property + def velocity(self) -> ProxyArray: + """Desired velocities [m/s or rad/s, depending on joint type].""" + return self._collection._joint_vel_target_ta + + @property + def effort(self) -> ProxyArray: + """Effort commands [N or N·m, depending on joint type].""" + return self._collection._joint_effort_target_ta + + def set_position_index( + self, + *, + value: torch.Tensor | wp.array(dtype=wp.float32), + joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, + env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, + full_data: bool = False, + ) -> None: + """Set desired positions using indices. + + Args: + value: Desired positions [m or rad, depending on joint type]. Shape is + ``(len(env_ids), len(joint_ids))``, or ``(num_instances, num_joints)`` when + :paramref:`full_data` is true. + joint_ids: Joint indices. Defaults to all joints. + env_ids: Environment indices. Defaults to all environments. + full_data: Whether :paramref:`value` is a full articulation command buffer. + """ + collection = self._collection + env_ids_resolved = collection._control.resolve_env_ids(env_ids) + joint_ids_resolved = collection._control.resolve_joint_ids(joint_ids) + self._write_index_target( + value, + env_ids_resolved, + joint_ids_resolved, + collection._joint_pos_target, + full_data=full_data, + command_name="position", + ) + + def set_velocity_index( + self, + *, + value: torch.Tensor | wp.array(dtype=wp.float32), + joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, + env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, + full_data: bool = False, + ) -> None: + """Set desired velocities using indices. + + Args: + value: Desired velocities [m/s or rad/s, depending on joint type]. Shape is + ``(len(env_ids), len(joint_ids))``, or ``(num_instances, num_joints)`` when + :paramref:`full_data` is true. + joint_ids: Joint indices. Defaults to all joints. + env_ids: Environment indices. Defaults to all environments. + full_data: Whether :paramref:`value` is a full articulation command buffer. + """ + collection = self._collection + env_ids_resolved = collection._control.resolve_env_ids(env_ids) + joint_ids_resolved = collection._control.resolve_joint_ids(joint_ids) + self._write_index_target( + value, + env_ids_resolved, + joint_ids_resolved, + collection._joint_vel_target, + full_data=full_data, + command_name="velocity", + ) + + def set_effort_index( + self, + *, + value: torch.Tensor | wp.array(dtype=wp.float32), + joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, + env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, + full_data: bool = False, + ) -> None: + """Set effort commands using indices. + + Args: + value: Effort commands [N or N·m, depending on joint type]. Shape is + ``(len(env_ids), len(joint_ids))``, or ``(num_instances, num_joints)`` when + :paramref:`full_data` is true. + joint_ids: Joint indices. Defaults to all joints. + env_ids: Environment indices. Defaults to all environments. + full_data: Whether :paramref:`value` is a full articulation command buffer. + """ + collection = self._collection + env_ids_resolved = collection._control.resolve_env_ids(env_ids) + joint_ids_resolved = collection._control.resolve_joint_ids(joint_ids) + self._write_index_target( + value, + env_ids_resolved, + joint_ids_resolved, + collection._joint_effort_target, + full_data=full_data, + command_name="effort", + ) + + def set_position_mask( + self, + *, + value: torch.Tensor | wp.array(dtype=wp.float32), + joint_mask: wp.array(dtype=wp.bool) | None = None, + env_mask: wp.array(dtype=wp.bool) | None = None, + ) -> None: + """Set desired positions using masks. + + Args: + value: Full articulation position commands [m or rad, depending on joint type]. Shape is + ``(num_instances, num_joints)``. + joint_mask: Joint selection mask. Defaults to all joints. + env_mask: Environment selection mask. Defaults to all environments. + """ + collection = self._collection + env_mask_resolved = self._resolve_mask(env_mask, collection._all_true_env_mask, "env_mask") + joint_mask_resolved = self._resolve_mask(joint_mask, collection._all_true_joint_mask, "joint_mask") + self._write_mask_target( + value, + env_mask_resolved, + joint_mask_resolved, + collection._joint_pos_target, + command_name="position", + ) + + def set_velocity_mask( + self, + *, + value: torch.Tensor | wp.array(dtype=wp.float32), + joint_mask: wp.array(dtype=wp.bool) | None = None, + env_mask: wp.array(dtype=wp.bool) | None = None, + ) -> None: + """Set desired velocities using masks. + + Args: + value: Full articulation velocity commands [m/s or rad/s, depending on joint type]. Shape is + ``(num_instances, num_joints)``. + joint_mask: Joint selection mask. Defaults to all joints. + env_mask: Environment selection mask. Defaults to all environments. + """ + collection = self._collection + env_mask_resolved = self._resolve_mask(env_mask, collection._all_true_env_mask, "env_mask") + joint_mask_resolved = self._resolve_mask(joint_mask, collection._all_true_joint_mask, "joint_mask") + self._write_mask_target( + value, + env_mask_resolved, + joint_mask_resolved, + collection._joint_vel_target, + command_name="velocity", + ) + + def set_effort_mask( + self, + *, + value: torch.Tensor | wp.array(dtype=wp.float32), + joint_mask: wp.array(dtype=wp.bool) | None = None, + env_mask: wp.array(dtype=wp.bool) | None = None, + ) -> None: + """Set effort commands using masks. + + Args: + value: Full articulation effort commands [N or N·m, depending on joint type]. Shape is + ``(num_instances, num_joints)``. + joint_mask: Joint selection mask. Defaults to all joints. + env_mask: Environment selection mask. Defaults to all environments. + """ + collection = self._collection + env_mask_resolved = self._resolve_mask(env_mask, collection._all_true_env_mask, "env_mask") + joint_mask_resolved = self._resolve_mask(joint_mask, collection._all_true_joint_mask, "joint_mask") + self._write_mask_target( + value, + env_mask_resolved, + joint_mask_resolved, + collection._joint_effort_target, + command_name="effort", + ) + + @staticmethod + def _resolve_mask( + mask: wp.array(dtype=wp.bool) | None, all_true_mask: wp.array(dtype=wp.bool), name: str + ) -> wp.array(dtype=wp.bool): + """Return the full selection mask for an optional ``wp.bool`` mask argument.""" + if mask is None: + return all_true_mask + if not isinstance(mask, wp.array) or mask.dtype != wp.bool: + raise TypeError(f"Expected '{name}' to be a wp.array of dtype wp.bool, got {type(mask)!r}.") + return mask + + def _write_index_target( + self, + target: torch.Tensor | wp.array(dtype=wp.float32), + env_ids: torch.Tensor | wp.array, + joint_ids: torch.Tensor | wp.array, + target_buffer: wp.array(dtype=wp.float32), + *, + full_data: bool, + command_name: str, + ) -> None: + collection = self._collection + expected_shape = ( + (collection.num_instances, collection.num_joints) if full_data else (env_ids.shape[0], joint_ids.shape[0]) + ) + collection._control.assert_shape_and_dtype(target, expected_shape, wp.float32, "target") + wp.launch( + actuator_kernels.write_2d_float_with_indices_kernel(env_ids, joint_ids), + dim=(env_ids.shape[0], joint_ids.shape[0]), + inputs=[target, env_ids, joint_ids, full_data], + outputs=[target_buffer], + device=collection.device, + ) + collection._control.stage_user_command(command_name, collection, env_ids, joint_ids, None, None) + + def _write_mask_target( + self, + target: torch.Tensor | wp.array(dtype=wp.float32), + env_mask: wp.array(dtype=wp.bool), + joint_mask: wp.array(dtype=wp.bool), + target_buffer: wp.array(dtype=wp.float32), + *, + command_name: str, + ) -> None: + collection = self._collection + collection._control.assert_shape_and_dtype_mask(target, (env_mask, joint_mask), wp.float32, "target") + wp.launch( + actuator_kernels.write_2d_float_with_mask, + dim=(env_mask.shape[0], joint_mask.shape[0]), + inputs=[target, env_mask, joint_mask], + outputs=[target_buffer], + device=collection.device, + ) + collection._control.stage_user_command(command_name, collection, None, None, env_mask, joint_mask) + + +class ActuatorOutputCommand: + """Processed commands produced for the simulated joints. + + These arrays contain submitted-command telemetry for Isaac Lab-managed + actuator models. Native controllers bypass the arrays, so they do not + provide submitted-command telemetry on a native path. + """ + + def __init__(self, collection: ActuatorCollection) -> None: + """Initialize the joint command view. + + Args: + collection: Owning actuator collection. + """ + self._collection = collection + + @property + def position(self) -> ProxyArray: + """Processed position commands [m or rad, depending on joint type].""" + return self._collection._joint_pos_target_sim_ta + + @property + def velocity(self) -> ProxyArray: + """Processed velocity commands [m/s or rad/s, depending on joint type].""" + return self._collection._joint_vel_target_sim_ta + + @property + def effort(self) -> ProxyArray: + """Processed effort commands [N or N·m, depending on joint type].""" + return self._collection._joint_effort_target_sim_ta diff --git a/source/isaaclab/isaaclab/actuators/actuator_control.py b/source/isaaclab/isaaclab/actuators/actuator_control.py new file mode 100644 index 000000000000..c831a1f5d33c --- /dev/null +++ b/source/isaaclab/isaaclab/actuators/actuator_control.py @@ -0,0 +1,470 @@ +# 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 + +"""Backend-neutral actuator control interfaces.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import torch +import warp as wp + +from isaaclab.utils.warp import ProxyArray + +from .actuator_base_cfg import ActuatorBaseCfg + +if TYPE_CHECKING: + from .actuator_collection import ActuatorCollection + from .newton.adapter import NewtonActuatorSelection + +_JOINT_PROPERTY_KEYS = ( + "stiffness", + "damping", + "armature", + "friction", + "dynamic_friction", + "viscous_friction", + "joint_effort_limit", + "joint_velocity_limit", +) +"""Keys of the joint-property payload exchanged between the collection and backend control. + +Each key maps to a group-shaped ``torch.Tensor``: + +- ``stiffness``: joint stiffness [N/m or N·m/rad, depending on joint type]. +- ``damping``: joint damping [N·s/m or N·m·s/rad, depending on joint type]. +- ``armature``: joint armature [kg or kg·m², depending on joint type]. +- ``friction``: backend-specific joint friction; see + :attr:`isaaclab.assets.ArticulationData.joint_friction_coeff` for the active + backend's convention. +- ``dynamic_friction``: backend-specific dynamic friction. PhysX interprets these as + dynamic friction efforts [N or N·m, depending on joint type], OVPhysX as + dimensionless Coulomb friction coefficients; Newton has no separate + dynamic-friction property, so its control adapter supplies zeros. +- ``viscous_friction``: passive joint damping [N·s/m or N·m·s/rad, depending on joint type]. +- ``joint_effort_limit``: joint effort limits [N or N·m, depending on joint type]. +- ``joint_velocity_limit``: joint velocity limits [m/s or rad/s, depending on joint type]. +""" + + +class ActuatorControl(ABC): + """Backend-neutral bridge used by :class:`~isaaclab.actuators.ActuatorCollection`.""" + + @staticmethod + def _normalize_index_sequence( + indices: Sequence[int] | slice | torch.Tensor | wp.array | None, + ) -> list[int] | slice | torch.Tensor | wp.array | None: + """Convert non-list integer sequences to the backend's list convention.""" + if isinstance(indices, Sequence) and not isinstance(indices, list): + return list(indices) + return indices + + @property + @abstractmethod + def num_instances(self) -> int: + """Number of articulation instances.""" + raise NotImplementedError + + @property + @abstractmethod + def num_joints(self) -> int: + """Number of articulation joints.""" + raise NotImplementedError + + @property + @abstractmethod + def num_fixed_tendons(self) -> int: + """Number of fixed tendons.""" + raise NotImplementedError + + @property + @abstractmethod + def device(self) -> str: + """Warp/Torch device string.""" + raise NotImplementedError + + @property + @abstractmethod + def joint_pos(self) -> ProxyArray: + """Current joint positions [m or rad, depending on joint type].""" + raise NotImplementedError + + @property + @abstractmethod + def joint_vel(self) -> ProxyArray: + """Current joint velocities [m/s or rad/s, depending on joint type].""" + raise NotImplementedError + + @property + def joint_stiffness(self) -> ProxyArray: + """Current joint stiffness values [N/m or N·m/rad, depending on joint type].""" + raise NotImplementedError( + "ActuatorControl.joint_stiffness is required for Lab implicit actuator execution. " + "The subclass must provide the current articulation-order joint stiffness as a ProxyArray." + ) + + @property + def joint_damping(self) -> ProxyArray: + """Current joint damping values [N·s/m or N·m·s/rad, depending on joint type].""" + raise NotImplementedError( + "ActuatorControl.joint_damping is required for Lab implicit actuator execution. " + "The subclass must provide the current articulation-order joint damping as a ProxyArray." + ) + + @property + def joint_effort_limits(self) -> ProxyArray: + """Current joint effort limits [N or N·m, depending on joint type].""" + raise NotImplementedError( + "ActuatorControl.joint_effort_limits is required for Lab implicit actuator execution. " + "The subclass must provide the current articulation-order joint effort limits as a ProxyArray." + ) + + @abstractmethod + def find_joints(self, name_keys: str | Sequence[str]) -> tuple[ProxyArray, list[str]]: + """Resolve joint name expressions to user-order joint indices and names. + + Args: + name_keys: Joint-name regular expressions. + + Returns: + Resolved joint indices and names in user order. + """ + raise NotImplementedError + + @abstractmethod + def resolve_env_ids( + self, + env_ids: Sequence[int] | torch.Tensor | wp.array | None, + ) -> torch.Tensor | wp.array: + """Resolve optional environment indices. + + Args: + env_ids: Environment indices. Defaults to all environments. + + Returns: + Device-local environment indices. + """ + raise NotImplementedError + + @abstractmethod + def resolve_joint_ids( + self, + joint_ids: Sequence[int] | torch.Tensor | wp.array | None, + ) -> torch.Tensor | wp.array: + """Resolve optional joint indices. + + Args: + joint_ids: Joint indices. Defaults to all joints. + + Returns: + Device-local joint indices. + """ + raise NotImplementedError + + @abstractmethod + def assert_shape_and_dtype( + self, + tensor: torch.Tensor | wp.array(dtype=wp.float32) | float, + shape: tuple[int, ...], + dtype: type, + name: str, + ) -> None: + """Validate tensor shape and dtype using the owning asset's policy. + + Args: + tensor: Tensor or scalar to validate. + shape: Required tensor shape. + dtype: Required Warp dtype. + name: Value name used in validation errors. + """ + raise NotImplementedError + + @abstractmethod + def assert_shape_and_dtype_mask( + self, + tensor: torch.Tensor | wp.array(dtype=wp.float32) | float, + masks: tuple[wp.array(dtype=wp.bool), ...], + dtype: type, + name: str, + ) -> None: + """Validate a full-sized mask-write tensor. + + Args: + tensor: Tensor or scalar to validate. + masks: Selection masks that define the required shape. + dtype: Required Warp dtype. + name: Value name used in validation errors. + """ + raise NotImplementedError + + @abstractmethod + def get_default_joint_properties(self, joint_ids: torch.Tensor | wp.array | slice) -> dict[str, torch.Tensor]: + """Return backend defaults used to construct one actuator group. + + Args: + joint_ids: Articulation joints in the actuator group. + + Returns: + Default properties for the selected joints, keyed by + :data:`_JOINT_PROPERTY_KEYS`. + """ + raise NotImplementedError + + @abstractmethod + def write_resolved_joint_properties( + self, + properties: dict[str, torch.Tensor], + joint_ids: torch.Tensor | wp.array | slice, + *, + implicit: bool, + native_managed: bool, + ) -> None: + """Write construction-resolved joint properties to the backend. + + Args: + properties: Resolved joint properties for one configured group, keyed by + :data:`_JOINT_PROPERTY_KEYS`. + joint_ids: Articulation joints in the configured group. + implicit: Whether the group uses an implicit solver drive. + native_managed: Whether the backend executes this group natively. + """ + raise NotImplementedError + + def stage_user_command( + self, + command_name: str, + collection: ActuatorCollection, + env_ids: torch.Tensor | wp.array | None, + joint_ids: torch.Tensor | wp.array | None, + env_mask: wp.array(dtype=wp.bool) | None, + joint_mask: wp.array(dtype=wp.bool) | None, + ) -> None: + """Stage a raw user command when the backend requires eager binding writes. + + Args: + command_name: Command field to stage. + collection: Collection that owns the command buffers. + env_ids: Selected environment indices, or None for a mask write. + joint_ids: Selected joint indices, or None for a mask write. + env_mask: Selected environments, or None for an index write. + joint_mask: Selected joints, or None for an index write. + """ + + @property + def native_actuator_path_active(self) -> bool: + """Whether backend handling replaces the Isaac Lab actuator loop.""" + return False + + def prepare_native_actuators( + self, collection: ActuatorCollection, actuator_cfgs: dict[str, ActuatorBaseCfg] + ) -> set[str]: + """Prepare backend-native actuators. + + Args: + collection: Collection being constructed. + actuator_cfgs: Configured actuator groups. + + Returns: + Names of groups managed by the backend. + """ + return set() + + def finalize_native_actuators(self, collection: ActuatorCollection) -> NewtonActuatorSelection | None: + """Finalize backend-native state after group construction. + + Args: + collection: Fully constructed actuator collection. + + Returns: + The Newton actuator selection produced by the backend's execution + setup (view, actuators, and joint ordering), or ``None`` when no + Newton actuators are active. The collection's parameter door + consumes this; controls perform no parameter access themselves. + """ + return None + + def compute_native_actuators(self, collection: ActuatorCollection, dt: float) -> bool: + """Compute backend-native actuator outputs. + + Args: + collection: Collection that owns actuator command and telemetry buffers. + dt: Physics step size [s]. + + Returns: + True when native handling replaced the standard Python actuator loop. + """ + return False + + @abstractmethod + def submit_commands(self, collection: ActuatorCollection) -> None: + """Submit processed command buffers to the backend. + + Args: + collection: Collection that owns the processed commands. + """ + raise NotImplementedError + + def reset_native_actuators(self, env_ids: Sequence[int] | slice) -> None: + """Reset backend-native actuator state. + + Args: + env_ids: Environments to reset. + """ + + +class ArticulationActuatorControl(ActuatorControl): + """Shared control adapter for articulation-backed actuator collections. + + This class implements the backend-independent forwarding and joint-property + plumbing used by articulation backends. Backend subclasses only need to + provide command submission and override the small hooks where their write + APIs differ. + + Args: + articulation: Articulation object that owns backend simulation handles. + """ + + def __init__(self, articulation): + self._articulation = articulation + self._native_actuator_path_active = False + + @property + def native_actuator_path_active(self) -> bool: + """Whether backend handling replaces the Isaac Lab actuator loop.""" + return self._native_actuator_path_active + + @property + def num_instances(self) -> int: + return self._articulation.num_instances + + @property + def num_joints(self) -> int: + return self._articulation.num_joints + + @property + def num_fixed_tendons(self) -> int: + return self._articulation.num_fixed_tendons + + @property + def device(self) -> str: + return self._articulation.device + + @property + def joint_pos(self) -> ProxyArray: + return self._articulation.data.joint_pos + + @property + def joint_vel(self) -> ProxyArray: + return self._articulation.data.joint_vel + + @property + def joint_stiffness(self) -> ProxyArray: + return self._articulation.data.joint_stiffness + + @property + def joint_damping(self) -> ProxyArray: + return self._articulation.data.joint_damping + + @property + def joint_effort_limits(self) -> ProxyArray: + return self._articulation.data.joint_effort_limits + + def find_joints(self, name_keys: str | Sequence[str]) -> tuple[ProxyArray, list[str]]: + return self._articulation.find_joints(name_keys, as_proxy=True) + + def resolve_env_ids( + self, + env_ids: Sequence[int] | torch.Tensor | wp.array | None, + ) -> torch.Tensor | wp.array: + return self._articulation._resolve_env_ids(self._normalize_index_sequence(env_ids)) + + def resolve_joint_ids( + self, + joint_ids: Sequence[int] | torch.Tensor | wp.array | None, + ) -> torch.Tensor | wp.array: + return self._articulation._resolve_joint_ids(self._normalize_index_sequence(joint_ids)) + + def assert_shape_and_dtype( + self, + tensor: torch.Tensor | wp.array(dtype=wp.float32) | float, + shape: tuple[int, ...], + dtype: type, + name: str, + ) -> None: + self._articulation.assert_shape_and_dtype(tensor, shape, dtype, name) + + def assert_shape_and_dtype_mask( + self, + tensor: torch.Tensor | wp.array(dtype=wp.float32) | float, + masks: tuple[wp.array(dtype=wp.bool), ...], + dtype: type, + name: str, + ) -> None: + self._articulation.assert_shape_and_dtype_mask(tensor, masks, dtype, name) + + def get_default_joint_properties(self, joint_ids: torch.Tensor | wp.array | slice) -> dict[str, torch.Tensor]: + if isinstance(joint_ids, wp.array): + joint_ids = wp.to_torch(joint_ids).to(device=self.device, dtype=torch.long) + data = self._articulation.data + stiffness = data.joint_stiffness.torch[:, joint_ids] + return { + "stiffness": stiffness.clone(), + "damping": data.joint_damping.torch[:, joint_ids].clone(), + "armature": data.joint_armature.torch[:, joint_ids].clone(), + "friction": data.joint_friction_coeff.torch[:, joint_ids].clone(), + "dynamic_friction": self._joint_property_or_zeros( + "joint_dynamic_friction_coeff", joint_ids, stiffness + ).clone(), + "viscous_friction": self._joint_property_or_zeros( + "joint_viscous_friction_coeff", joint_ids, stiffness + ).clone(), + "joint_effort_limit": data.joint_effort_limits.torch[:, joint_ids].clone(), + "joint_velocity_limit": data.joint_vel_limits.torch[:, joint_ids].clone(), + } + + def write_resolved_joint_properties( + self, + properties: dict[str, torch.Tensor], + joint_ids: torch.Tensor | wp.array | slice, + *, + implicit: bool, + native_managed: bool, + ) -> None: + articulation = self._articulation + articulation.write_joint_effort_limit_to_sim_index( + limits=properties["joint_effort_limit"], + joint_ids=joint_ids, + ) + articulation.write_joint_velocity_limit_to_sim_index( + limits=properties["joint_velocity_limit"], + joint_ids=joint_ids, + ) + articulation.write_joint_armature_to_sim_index(armature=properties["armature"], joint_ids=joint_ids) + articulation.write_joint_friction_coefficient_to_sim_index( + joint_friction_coeff=properties["friction"], + joint_dynamic_friction_coeff=properties["dynamic_friction"], + joint_viscous_friction_coeff=properties["viscous_friction"], + joint_ids=joint_ids, + ) + if implicit and not native_managed: + articulation.write_joint_stiffness_to_sim_index(stiffness=properties["stiffness"], joint_ids=joint_ids) + articulation.write_joint_damping_to_sim_index(damping=properties["damping"], joint_ids=joint_ids) + else: + articulation.write_joint_stiffness_to_sim_index(stiffness=0.0, joint_ids=joint_ids) + articulation.write_joint_damping_to_sim_index(damping=0.0, joint_ids=joint_ids) + + def _joint_property_or_zeros( + self, + attr_name: str, + joint_ids: torch.Tensor | wp.array | slice, + reference: torch.Tensor, + ) -> torch.Tensor: + joint_property = getattr(self._articulation.data, attr_name, None) + if joint_property is None: + return torch.zeros_like(reference) + return joint_property.torch[:, joint_ids] diff --git a/source/isaaclab/isaaclab/actuators/actuator_kernels.py b/source/isaaclab/isaaclab/actuators/actuator_kernels.py new file mode 100644 index 000000000000..b68d51e78220 --- /dev/null +++ b/source/isaaclab/isaaclab/actuators/actuator_kernels.py @@ -0,0 +1,146 @@ +# 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 + +"""Warp kernels used by actuator collections.""" + +from __future__ import annotations + +from typing import Any + +import torch +import warp as wp + +from isaaclab.utils.warp.index_kernel import IndexKernelDispatcher + + +@wp.kernel(enable_backward=False) +def write_2d_float_with_indices( + source: wp.array2d(dtype=wp.float32), + env_ids: wp.array(dtype=Any), + joint_ids: wp.array(dtype=Any), + full_data: bool, + target: wp.array2d(dtype=wp.float32), +): + """Write 2-D float data into a target buffer using environment and joint indices.""" + env_i, joint_i = wp.tid() + env_id = env_ids[env_i] + joint_id = joint_ids[joint_i] + if full_data: + target[env_id, joint_id] = source[env_id, joint_id] + else: + target[env_id, joint_id] = source[env_i, joint_i] + + +_WRITE_2D_FLOAT_WITH_INDICES_DISPATCHER = IndexKernelDispatcher(write_2d_float_with_indices, ("env_ids", "joint_ids")) + + +def write_2d_float_with_indices_kernel( + env_ids: torch.Tensor | wp.array, joint_ids: torch.Tensor | wp.array +) -> wp.Kernel: + """Select the indexed float writer for the selector dtypes. + + Args: + env_ids: Environment indices. + joint_ids: Joint indices. + + Returns: + Warp kernel specialized for the two selector dtypes. + """ + return _WRITE_2D_FLOAT_WITH_INDICES_DISPATCHER.select(env_ids, joint_ids) + + +@wp.kernel(enable_backward=False) +def write_2d_float_with_mask( + source: wp.array2d(dtype=wp.float32), + env_mask: wp.array(dtype=wp.bool), + joint_mask: wp.array(dtype=wp.bool), + target: wp.array2d(dtype=wp.float32), +): + """Write full-sized 2-D float data into a target buffer using masks.""" + env_id, joint_id = wp.tid() + if env_mask[env_id] and joint_mask[joint_id]: + target[env_id, joint_id] = source[env_id, joint_id] + + +@wp.kernel(enable_backward=False) +def scatter_explicit_actuator_outputs( + source_pos: wp.array2d(dtype=wp.float32), + source_vel: wp.array2d(dtype=wp.float32), + source_effort: wp.array2d(dtype=wp.float32), + source_computed_effort: wp.array2d(dtype=wp.float32), + source_applied_effort: wp.array2d(dtype=wp.float32), + source_velocity_limit: wp.array2d(dtype=wp.float32), + joint_indices: wp.array(dtype=wp.int32), + target_pos: wp.array2d(dtype=wp.float32), + target_vel: wp.array2d(dtype=wp.float32), + target_effort: wp.array2d(dtype=wp.float32), + target_computed_effort: wp.array2d(dtype=wp.float32), + target_applied_effort: wp.array2d(dtype=wp.float32), + target_velocity_limit: wp.array2d(dtype=wp.float32), +): + """Scatter explicit actuator outputs into full articulation buffers. + + Command outputs are scattered only when present. Computed and applied effort + and velocity-limit telemetry are always published. Explicit actuator models (for example + :class:`~isaaclab.actuators.IdealPDActuator`) clear the position and velocity + commands after computing the effort, so those sources may be null. + """ + env_id, source_joint_id = wp.tid() + target_joint_id = joint_indices[source_joint_id] + if source_pos: + target_pos[env_id, target_joint_id] = source_pos[env_id, source_joint_id] + if source_vel: + target_vel[env_id, target_joint_id] = source_vel[env_id, source_joint_id] + if source_effort: + target_effort[env_id, target_joint_id] = source_effort[env_id, source_joint_id] + target_computed_effort[env_id, target_joint_id] = source_computed_effort[env_id, source_joint_id] + target_applied_effort[env_id, target_joint_id] = source_applied_effort[env_id, source_joint_id] + target_velocity_limit[env_id, target_joint_id] = source_velocity_limit[env_id, source_joint_id] + + +@wp.kernel(enable_backward=False) +def compute_implicit_actuator_batch( + command_pos: wp.array2d(dtype=wp.float32), + command_vel: wp.array2d(dtype=wp.float32), + command_effort: wp.array2d(dtype=wp.float32), + joint_pos: wp.array2d(dtype=wp.float32), + joint_vel: wp.array2d(dtype=wp.float32), + joint_stiffness: wp.array2d(dtype=wp.float32), + joint_damping: wp.array2d(dtype=wp.float32), + joint_effort_limits: wp.array2d(dtype=wp.float32), + velocity_limit: wp.array2d(dtype=wp.float32), + joint_indices: wp.array(dtype=wp.int32), + batch_computed_effort: wp.array2d(dtype=wp.float32), + batch_applied_effort: wp.array2d(dtype=wp.float32), + target_pos: wp.array2d(dtype=wp.float32), + target_vel: wp.array2d(dtype=wp.float32), + target_effort: wp.array2d(dtype=wp.float32), + computed_effort: wp.array2d(dtype=wp.float32), + applied_effort: wp.array2d(dtype=wp.float32), + soft_velocity_limit: wp.array2d(dtype=wp.float32), +): + """Compute and publish one implicit actuator execution batch.""" + env_id, batch_joint_id = wp.tid() + joint_id = joint_indices[batch_joint_id] + + position_target = command_pos[env_id, joint_id] + velocity_target = command_vel[env_id, joint_id] + feedforward = command_effort[env_id, joint_id] + effort = ( + joint_stiffness[env_id, joint_id] * (position_target - joint_pos[env_id, joint_id]) + + joint_damping[env_id, joint_id] * (velocity_target - joint_vel[env_id, joint_id]) + + feedforward + ) + limit = joint_effort_limits[env_id, joint_id] + clamped_effort = wp.clamp(effort, -limit, limit) + + batch_computed_effort[env_id, batch_joint_id] = effort + batch_applied_effort[env_id, batch_joint_id] = clamped_effort + target_pos[env_id, joint_id] = position_target + target_vel[env_id, joint_id] = velocity_target + target_effort[env_id, joint_id] = feedforward + computed_effort[env_id, joint_id] = effort + applied_effort[env_id, joint_id] = clamped_effort + soft_velocity_limit[env_id, joint_id] = velocity_limit[env_id, batch_joint_id] diff --git a/source/isaaclab/isaaclab/actuators/actuator_pd.py b/source/isaaclab/isaaclab/actuators/actuator_pd.py index fda0add21b95..9106bea85fcd 100644 --- a/source/isaaclab/isaaclab/actuators/actuator_pd.py +++ b/source/isaaclab/isaaclab/actuators/actuator_pd.py @@ -6,17 +6,20 @@ from __future__ import annotations import logging +import warnings from collections.abc import Sequence -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar import torch from isaaclab.utils import DelayBuffer, LinearInterpolation from isaaclab.utils.types import ArticulationActions -from .actuator_base import ActuatorBase +from ._compat import _limits_equal +from .actuator_base import ActuatorBase, resolve_joint_parameter if TYPE_CHECKING: + from .actuator_control import ActuatorControl from .actuator_pd_cfg import ( DCMotorCfg, DelayedPDActuatorCfg, @@ -36,66 +39,160 @@ class ImplicitActuator(ActuatorBase): """Implicit actuator model that is handled by the simulation. - This performs a similar function as the :class:`IdealPDActuator` class. However, the PD control is handled - implicitly by the simulation which performs continuous-time integration of the PD control law. This is - generally more accurate than the explicit PD control law used in :class:`IdealPDActuator` when the simulation - time-step is large. - - The articulation class sets the stiffness and damping parameters from the implicit actuator configuration - into the simulation. Thus, the class does not perform its own computations on the joint action that - needs to be applied to the simulation. However, it computes the approximate torques for the actuated joint - since PhysX does not expose this quantity explicitly. - - .. caution:: - - The class is only provided for consistency with the other actuator models. It does not implement any - functionality and should not be used. All values should be set to the simulation directly. + The articulation writes the configured gains and solver limits to the + backend, whose discrete solver applies the joint drive. This model also + computes approximate effort telemetry from the current state because the + solver does not expose the applied joint effort on every backend. """ cfg: ImplicitActuatorCfg """The configuration for the actuator model.""" - def __init__(self, cfg: ImplicitActuatorCfg, *args, **kwargs): - # effort limits - if cfg.effort_limit_sim is None and cfg.effort_limit is not None: - cfg.effort_limit_sim = cfg.effort_limit - elif cfg.effort_limit_sim is not None and cfg.effort_limit is None: - cfg.effort_limit = cfg.effort_limit_sim - - # velocity limits - # 'velocity_limit' is the joint's peak velocity (the actuator's rated speed - # reflected at the joint): it feeds the data buffers - # (:attr:`ArticulationData.soft_joint_vel_limits`, read by e.g. the - # ``joint_vel_out_of_limit`` termination) but is NOT pushed to the physics - # solver. 'velocity_limit_sim' is a solver-level hard clamp (PhysX - # ``maxJointVelocity``) with no physical counterpart -- a physical actuator - # limits joint speed through its torque curve, not a kinematic clamp. The - # two are therefore resolved independently: when only the sim clamp is - # given, it doubles as the joint velocity limit so the data buffers stay - # meaningful; when only 'velocity_limit' is given, the solver keeps the - # USD-authored clamp. - if cfg.velocity_limit_sim is not None and cfg.velocity_limit is None: - cfg.velocity_limit = cfg.velocity_limit_sim - elif cfg.velocity_limit is not None and cfg.velocity_limit_sim is None: - # notify about the behavior change: this value used to be ignored for implicit actuators - logger.warning( - "The object has a value for 'velocity_limit'. Previously, this value" - " was ignored for implicit actuators. It now populates the joint velocity-limit data buffers" - " (e.g. 'soft_joint_vel_limits' used by velocity-limit terminations and rewards), but it is" - " still not pushed to the physics solver. To set a solver-level velocity clamp, please use" - " 'velocity_limit_sim'." + is_implicit_model: ClassVar[bool] = True + + def __init__( + self, + cfg: ImplicitActuatorCfg, + joint_names: list[str], + joint_ids: slice | torch.Tensor, + num_envs: int, + device: str, + stiffness: torch.Tensor | float = 0.0, + damping: torch.Tensor | float = 0.0, + joint_effort_limit: torch.Tensor | float | None = None, + actuator_velocity_limit: torch.Tensor | float | None = None, + effort_limit: torch.Tensor | float | None = None, # TODO: Deprecated. Remove in 4.0. + velocity_limit: torch.Tensor | float | None = None, # TODO: Deprecated. Remove in 4.0. + ): + """Initialize the implicit actuator. + + Args: + cfg: The configuration of the actuator model. + joint_names: The joint names in the articulation. + joint_ids: The joint indices in the articulation. If :obj:`slice(None)`, then all + the joints in the articulation are part of the group. + num_envs: Number of articulations in the view. + device: Device used for processing. + stiffness: Default joint stiffness [N/m or N·m/rad, depending on joint type]. + damping: Default joint damping [N·s/m or N·m·s/rad, depending on joint type]. + joint_effort_limit: Default solver joint effort limit [N or N·m, depending on + joint type]. Defaults to infinity. + actuator_velocity_limit: Default actuator velocity limit [m/s or rad/s, + depending on joint type]. Defaults to infinity. + effort_limit: Deprecated alias for :paramref:`joint_effort_limit`. + velocity_limit: Deprecated alias for :paramref:`actuator_velocity_limit`. + """ + # TODO: Deprecated. Remove in 4.0. + if effort_limit is not None: + warnings.warn( + "The effort_limit constructor argument is deprecated. Use joint_effort_limit instead; " + "effort_limit will be removed in 4.0.", + DeprecationWarning, + stacklevel=2, + ) + if joint_effort_limit is not None and not _limits_equal(joint_effort_limit, effort_limit): + raise ValueError( + "Received conflicting joint_effort_limit and deprecated effort_limit constructor arguments." + ) + joint_effort_limit = effort_limit + # the base class resolves deprecated configuration aliases and skips the actuator effort + # limit, which implicit models expose as a live projection of the articulation limit. + super().__init__( + cfg, joint_names, joint_ids, num_envs, device, None, actuator_velocity_limit, None, velocity_limit + ) + # construction values, used until the group is bound to articulation storage. + self._construction_stiffness = resolve_joint_parameter( + self.cfg.stiffness, stiffness, joint_names, num_envs, device + ) + self._construction_damping = resolve_joint_parameter(self.cfg.damping, damping, joint_names, num_envs, device) + self._construction_joint_effort_limit = resolve_joint_parameter( + self.cfg.joint_effort_limit, + joint_effort_limit if joint_effort_limit is not None else torch.inf, + joint_names, + num_envs, + device, + ) + # optional rated (model-facing) effort limit, kept separate from the solver clamp. + # When unset, :attr:`actuator_effort_limit` tracks the live solver limit instead. + if self.cfg.actuator_effort_limit is not None: + self._rated_effort_limit: torch.Tensor | None = resolve_joint_parameter( + self.cfg.actuator_effort_limit, None, joint_names, num_envs, device ) + else: + self._rated_effort_limit = None + # full articulation-order joint property tensors, set by :meth:`_bind_actuator_parameters`. + self._stiffness: torch.Tensor | None = None + self._damping: torch.Tensor | None = None + self._joint_effort_limit: torch.Tensor | None = None - # set implicit actuator model flag - ImplicitActuator.is_implicit_model = True - # call the base class - super().__init__(cfg, *args, **kwargs) + """ + Properties. + """ + + @property + def stiffness(self) -> torch.Tensor: + """Current joint stiffness values [N/m or N·m/rad, depending on joint type].""" + if self._stiffness is None: + return self._construction_stiffness + return self._stiffness[:, self.joint_indices] + + @stiffness.setter + def stiffness(self, value: torch.Tensor) -> None: + self._warn_articulation_owned_write("stiffness", "write_joint_stiffness_to_sim_index") + + @property + def damping(self) -> torch.Tensor: + """Current joint damping values [N·s/m or N·m·s/rad, depending on joint type].""" + if self._damping is None: + return self._construction_damping + return self._damping[:, self.joint_indices] + + @damping.setter + def damping(self, value: torch.Tensor) -> None: + self._warn_articulation_owned_write("damping", "write_joint_damping_to_sim_index") + + @property + def joint_effort_limit(self) -> torch.Tensor: + """Current joint effort limits [N or N·m, depending on joint type].""" + if self._joint_effort_limit is None: + return self._construction_joint_effort_limit + return self._joint_effort_limit[:, self.joint_indices] + + @joint_effort_limit.setter + def joint_effort_limit(self, value: torch.Tensor) -> None: + self._warn_articulation_owned_write("joint_effort_limit", "write_joint_effort_limit_to_sim_index") + + @property + def actuator_effort_limit(self) -> torch.Tensor: + """Actuator effort limit [N or N·m, depending on joint type]. + + The actuator's rated force/torque reflected at the joint when configured through + :attr:`~isaaclab.actuators.ActuatorBaseCfg.actuator_effort_limit`; it clips the + effort telemetry but is not pushed to the solver. When unset, it tracks the live + articulation joint effort limit (:attr:`joint_effort_limit`). + """ + if self._rated_effort_limit is not None: + return self._rated_effort_limit + return self.joint_effort_limit + + @actuator_effort_limit.setter + def actuator_effort_limit(self, value: torch.Tensor) -> None: + self._warn_articulation_owned_write("joint_effort_limit", "write_joint_effort_limit_to_sim_index") + + def _warn_articulation_owned_write(self, name: str, writer_name: str) -> None: + """Warn that an articulation-owned joint property assignment is ignored.""" + warnings.warn( + f"ImplicitActuator.{name} is articulation-owned and the assignment is ignored. Use " + f"Articulation.{writer_name}() or randomize_actuator_gains() to update it.", + UserWarning, + stacklevel=3, + ) """ Operations. """ - def reset(self, *args, **kwargs): + def reset(self, env_ids: Sequence[int] | None = None): # This is a no-op. There is no state to reset for implicit actuators. pass @@ -104,19 +201,22 @@ def compute( ) -> ArticulationActions: """Process the actuator group actions and compute the articulation actions. - In case of implicit actuator, the control action is directly returned as the computed action. - This function is a no-op and does not perform any computation on the input control action. - However, it computes the approximate torques for the actuated joint since PhysX does not compute - this quantity explicitly. + For an implicit actuator, the desired control action is returned unchanged because the + physics solver applies the PD drive. This method still computes approximate computed and + applied effort telemetry from the current joint state. That telemetry may differ from the + effort applied internally by the solver. Args: - control_action: The joint action instance comprising of the desired joint positions, joint velocities - and (feed-forward) joint efforts. - joint_pos: The current joint positions of the joints in the group. Shape is (num_envs, num_joints). - joint_vel: The current joint velocities of the joints in the group. Shape is (num_envs, num_joints). + control_action: Desired joint positions [m or rad, depending on joint type], velocities [m/s or rad/s, + depending on joint type], and feed-forward efforts [N or N·m, depending on joint type]. + joint_pos: Current joint positions [m or rad, depending on joint type], shape + ``(num_envs, num_joints)``. + joint_vel: Current joint velocities [m/s or rad/s, depending on joint type], shape + ``(num_envs, num_joints)``. Returns: - The computed desired joint positions, joint velocities and joint efforts. + Desired joint positions [m or rad, depending on joint type], velocities [m/s or rad/s, depending on + joint type], and efforts [N or N·m, depending on joint type]. """ # store approximate torques for reward computation error_pos = control_action.joint_positions - joint_pos @@ -126,6 +226,24 @@ def compute( self.applied_effort = self._clip_effort(self.computed_effort) return control_action + def _clip_effort(self, effort: torch.Tensor) -> torch.Tensor: + """Clip telemetry using the articulation joint effort limit.""" + joint_effort_limit = self.joint_effort_limit + return torch.clip(effort, min=-joint_effort_limit, max=joint_effort_limit) + + def _bind_actuator_parameters(self, control: ActuatorControl) -> None: + """Bind joint-property reads to live articulation-owned storage. + + Stores references to the full articulation-order tensors instead of per-group + slices: with non-contiguous joint indices, slicing would materialize a copy and + the reference to the articulation storage would be lost. Property reads index + the full tensors on access, so they always observe the articulation's current + values. + """ + self._stiffness = control.joint_stiffness.torch + self._damping = control.joint_damping.torch + self._joint_effort_limit = control.joint_effort_limits.torch + """ Explicit Actuator Models. @@ -145,22 +263,54 @@ class IdealPDActuator(ActuatorBase): are the current joint positions and velocities, :math:`q_{des}`, :math:`\dot{q}_{des}` and :math:`\tau_{ff}` are the desired joint positions, velocities and torques commands. - The clipping model is based on the maximum torque applied by the motor. It is implemented as: + The model clips the resulting joint effort directly to + :attr:`actuator_effort_limit`: .. math:: - \tau_{j, max} & = \gamma \times \tau_{motor, max} \\ - \tau_{j, applied} & = clip(\tau_{computed}, -\tau_{j, max}, \tau_{j, max}) + \tau_{j, applied} = clip(\tau_{j, computed}, -\tau_{max}, \tau_{max}) - where the clipping function is defined as :math:`clip(x, x_{min}, x_{max}) = min(max(x, x_{min}), x_{max})`. - The parameters :math:`\gamma` is the gear ratio of the gear box connecting the motor and the actuated joint ends, - and :math:`\tau_{motor, max}` is the maximum motor effort possible. These parameters are read from - the configuration instance passed to the class. + where :math:`\tau_{max}` is the configured joint-side effort limit [N or + N·m, depending on joint type]. """ cfg: IdealPDActuatorCfg """The configuration for the actuator model.""" + actuator_effort_limit: torch.Tensor + """Actuator-model effort clipping limit [N or N·m, depending on joint type]. + + Shape is (num_envs, num_joints). + """ + + def __init__( + self, + cfg: IdealPDActuatorCfg, + joint_names: list[str], + joint_ids: slice | torch.Tensor, + num_envs: int, + device: str, + stiffness: torch.Tensor | float = 0.0, + damping: torch.Tensor | float = 0.0, + actuator_effort_limit: torch.Tensor | float | None = None, + actuator_velocity_limit: torch.Tensor | float | None = None, + effort_limit: torch.Tensor | float | None = None, # TODO: Deprecated. Remove in 4.0. + velocity_limit: torch.Tensor | float | None = None, # TODO: Deprecated. Remove in 4.0. + ): + super().__init__( + cfg, + joint_names, + joint_ids, + num_envs, + device, + actuator_effort_limit, + actuator_velocity_limit, + effort_limit, # TODO: Deprecated. Remove in 4.0. + velocity_limit, # TODO: Deprecated. Remove in 4.0. + ) + self.stiffness = resolve_joint_parameter(self.cfg.stiffness, stiffness, joint_names, num_envs, device) + self.damping = resolve_joint_parameter(self.cfg.damping, damping, joint_names, num_envs, device) + """ Operations. """ @@ -198,14 +348,18 @@ class DCMotor(IdealPDActuator): A DC motor characteristics are defined by the following parameters: - * No-load speed (:math:`\dot{q}_{motor, max}`) : The maximum-rated speed of the motor at - zero torque (:attr:`velocity_limit`). + * No-load speed (:math:`\dot{q}_{motor, max}`) [m/s or rad/s, depending on + joint type]: The maximum-rated speed of the motor at zero torque + (:attr:`actuator_velocity_limit`). * Stall torque (:math:`\tau_{motor, stall}`): The maximum-rated torque produced at - zero speed (:attr:`saturation_effort`). - * Continuous torque (:math:`\tau_{motor, con}`): The maximum torque that can be outputted for a short period. - This is often enforced on the current drives for a DC motor to limit overheating, prevent mechanical damage, - or enforced by electrical limitations (:attr:`effort_limit`). - * Corner velocity (:math:`V_{c}`): The velocity where the torque-speed curve intersects with continuous torque. + zero speed [N or N·m, depending on joint type] (:attr:`saturation_effort`). + * Continuous torque (:math:`\tau_{motor, con}`) [N or N·m, depending on + joint type]: The maximum torque that can be outputted for a short period. + This is often enforced on the current drives for a DC motor to limit + overheating, prevent mechanical damage, or enforced by electrical + limitations (:attr:`actuator_effort_limit`). + * Corner velocity (:math:`V_{c}`) [m/s or rad/s, depending on joint type]: + The velocity where the torque-speed curve intersects with continuous torque. Based on these parameters, the instantaneous minimum and maximum torques for velocities between corner velocities (where torque-speed curve intersects with continuous torque) are defined as follows: @@ -231,7 +385,8 @@ class DCMotor(IdealPDActuator): \tau_{j, applied} = clip(\tau_{computed}, \tau_{j, min}(\dot{q}), \tau_{j, max}(\dot{q})) If the velocity of the joint is outside corner velocities (this would be due to external forces) the - applied output torque will be driven to the Continuous Torque (`effort_limit`). + applied output torque will be driven to the continuous torque + (:attr:`actuator_effort_limit`). The figure below demonstrates the clipping action for example (velocity, torque) pairs. @@ -251,15 +406,17 @@ def __init__(self, cfg: DCMotorCfg, *args, **kwargs): if self.cfg.saturation_effort is None: raise ValueError("The saturation_effort must be provided for the DC motor actuator model.") self._saturation_effort = self.cfg.saturation_effort - # find the velocity on the torque-speed curve that intersects effort_limit in the second and fourth quadrant - self._vel_at_effort_lim = self.velocity_limit * (1 + self.effort_limit / self._saturation_effort) + # check that quantities are provided + if self.cfg.actuator_velocity_limit is None: + raise ValueError("The velocity limit must be provided for the DC motor actuator model.") + # Find the velocity where the torque-speed curve intersects actuator_effort_limit. + self._vel_at_effort_lim = self.actuator_velocity_limit * ( + 1 + self.actuator_effort_limit / self._saturation_effort + ) # prepare joint vel buffer for max effort computation self._joint_vel = torch.zeros_like(self.computed_effort) # create buffer for zeros effort self._zeros_effort = torch.zeros_like(self.computed_effort) - # check that quantities are provided - if self.cfg.velocity_limit is None: - raise ValueError("The velocity limit must be provided for the DC motor actuator model.") """ Operations. @@ -281,12 +438,12 @@ def _clip_effort(self, effort: torch.Tensor) -> torch.Tensor: # save current joint vel self._joint_vel[:] = torch.clip(self._joint_vel, min=-self._vel_at_effort_lim, max=self._vel_at_effort_lim) # compute torque limits - torque_speed_top = self._saturation_effort * (1.0 - self._joint_vel / self.velocity_limit) - torque_speed_bottom = self._saturation_effort * (-1.0 - self._joint_vel / self.velocity_limit) + torque_speed_top = self._saturation_effort * (1.0 - self._joint_vel / self.actuator_velocity_limit) + torque_speed_bottom = self._saturation_effort * (-1.0 - self._joint_vel / self.actuator_velocity_limit) # -- max limit - max_effort = torch.clip(torque_speed_top, max=self.effort_limit) + max_effort = torch.clip(torque_speed_top, max=self.actuator_effort_limit) # -- min limit - min_effort = torch.clip(torque_speed_bottom, min=-self.effort_limit) + min_effort = torch.clip(torque_speed_bottom, min=-self.actuator_effort_limit) # clip the torques based on the motor limits clamped = torch.clip(effort, min=min_effort, max=max_effort) return clamped @@ -355,9 +512,12 @@ def compute( class RemotizedPDActuator(DelayedPDActuator): """Ideal PD actuator with angle-dependent torque limits. - This class extends the :class:`DelayedPDActuator` class by adding angle-dependent torque limits to the actuator. - The torque limits are applied by querying a lookup table describing the relationship between the joint angle - and the maximum output torque. The lookup table is provided in the configuration instance passed to the class. + This class extends :class:`DelayedPDActuator` with angle-dependent effort + limits [N or N·m, depending on joint type]. The limits are applied by + querying a lookup table describing the relationship between joint angle + [m or rad, depending on joint type] and maximum output effort [N or N·m, + depending on joint type]. The lookup table is provided in the configuration + instance passed to the class. The torque limits are interpolated based on the current joint positions and applied to the actuator commands. """ @@ -371,17 +531,11 @@ def __init__( device: str, stiffness: torch.Tensor | float = 0.0, damping: torch.Tensor | float = 0.0, - armature: torch.Tensor | float = 0.0, - friction: torch.Tensor | float = 0.0, - dynamic_friction: torch.Tensor | float = 0.0, - viscous_friction: torch.Tensor | float = 0.0, - effort_limit: torch.Tensor | float = torch.inf, - velocity_limit: torch.Tensor | float = torch.inf, + actuator_effort_limit: torch.Tensor | float | None = None, + actuator_velocity_limit: torch.Tensor | float | None = None, + effort_limit: torch.Tensor | float | None = None, # TODO: Deprecated. Remove in 4.0. + velocity_limit: torch.Tensor | float | None = None, # TODO: Deprecated. Remove in 4.0. ): - # remove effort and velocity box constraints from the base class - cfg.effort_limit = torch.inf - cfg.velocity_limit = torch.inf - # call the base method and set default effort_limit and velocity_limit to inf super().__init__( cfg, joint_names, @@ -390,13 +544,15 @@ def __init__( device, stiffness, damping, - armature, - friction, - dynamic_friction, - viscous_friction, - effort_limit, - velocity_limit, + actuator_effort_limit, + actuator_velocity_limit, + effort_limit, # TODO: Deprecated. Remove in 4.0. + velocity_limit, # TODO: Deprecated. Remove in 4.0. ) + # This model has no box constraints: the angle-dependent lookup table below governs + # effort clipping, so the parsed actuator limits are replaced with infinity. + self.actuator_effort_limit = torch.full_like(self.actuator_effort_limit, torch.inf) + self.actuator_velocity_limit = torch.full_like(self.actuator_velocity_limit, torch.inf) self._joint_parameter_lookup = torch.tensor(cfg.joint_parameter_lookup, device=device) # define remotized joint torque limit self._torque_limit = LinearInterpolation(self.angle_samples, self.max_torque_samples, device=device) @@ -407,14 +563,17 @@ def __init__( @property def angle_samples(self) -> torch.Tensor: + """Lookup joint positions [m or rad, depending on joint type].""" return self._joint_parameter_lookup[:, 0] @property def transmission_ratio_samples(self) -> torch.Tensor: + """Dimensionless lookup transmission ratios.""" return self._joint_parameter_lookup[:, 1] @property def max_torque_samples(self) -> torch.Tensor: + """Lookup effort limits [N or N·m, depending on joint type].""" return self._joint_parameter_lookup[:, 2] """ diff --git a/source/isaaclab/isaaclab/actuators/newton/__init__.py b/source/isaaclab/isaaclab/actuators/newton/__init__.py new file mode 100644 index 000000000000..01560a03afd7 --- /dev/null +++ b/source/isaaclab/isaaclab/actuators/newton/__init__.py @@ -0,0 +1,42 @@ +# 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-native actuator integration for Isaac Lab. + +Public API surface: + +* :class:`~isaaclab.actuators.newton.adapter.NewtonActuatorAdapter` — + the actuator adapter used by Newton and the host adapters. Newton + constructs it directly from ``model.actuators``; PhysX and OVPhysX use + :meth:`~NewtonActuatorAdapter.from_usd` to build actuators from authored + ``NewtonActuator`` USD prims. +* :class:`~isaaclab.actuators.newton.physx_wrapper.PhysxActuatorWrapper` + — flat-array wrapper that satisfies the Newton actuator + ``sim_state`` / ``sim_control`` protocol on PhysX and OVPhysX. +* :func:`~isaaclab.actuators.newton.kernels.build_implicit_dof_mask` — + builds the per-DOF implicit-actuator mask consumed by the in-graph + post-actuator kernel. +* :func:`~isaaclab.actuators.newton.adapter.read_group_parameter` / + :func:`~isaaclab.actuators.newton.adapter.write_group_parameter` — + group-scoped, user-ordered access to Newton actuator parameters through + the selection API; the raw alternative is the Newton actuator object + returned by the actuator collection mapping. + +USD authoring lives on the schema side as +:func:`~isaaclab.sim.schemas.define_actuator_properties`; each backend calls +it through :meth:`ArticulationCfg._post_spawn`. +""" + +from .adapter import NewtonActuatorAdapter, read_group_parameter, write_group_parameter +from .kernels import build_implicit_dof_mask +from .physx_wrapper import PhysxActuatorWrapper + +__all__ = [ + "NewtonActuatorAdapter", + "PhysxActuatorWrapper", + "build_implicit_dof_mask", + "read_group_parameter", + "write_group_parameter", +] diff --git a/source/isaaclab/isaaclab/actuators/newton/adapter.py b/source/isaaclab/isaaclab/actuators/newton/adapter.py new file mode 100644 index 000000000000..7498334c01ac --- /dev/null +++ b/source/isaaclab/isaaclab/actuators/newton/adapter.py @@ -0,0 +1,692 @@ +# 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-actuator adapter shared by Newton, PhysX, and OVPhysX. + +Owns the actuator-state lifecycle, the pre-clamp computed-effort buffer, +and the per-step ``step`` / ``reset`` / ``finalize`` calls. The +:meth:`~NewtonActuatorAdapter.from_usd` classmethod parses +``NewtonActuator`` USD prims for PhysX and OVPhysX. Newton populates +``model.actuators`` itself. + +DR gain updates bypass the adapter — the articulation writes straight +to controller arrays. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, TypeAlias + +import numpy as np +import torch +import warp as wp +from newton import Model +from newton._src.utils.selection import FrequencyLayout +from newton.actuators import Actuator, Clamping, Delay +from newton.selection import ArticulationView + +from .kernels import ( + build_implicit_dof_mask, + build_per_dof_env_mask_kernel, + set_mask_kernel, + zero_at_indices_kernel, +) + +if TYPE_CHECKING: + from isaaclab.actuators import ActuatorCollection + +# --------------------------------------------------------------------------- +# Abstract base — backend-independent logic +# --------------------------------------------------------------------------- + + +class NewtonActuatorAdapter: + """Adapter that wraps a list of :class:`newton.actuators.Actuator`. + + Owns the actuator-state lifecycle, DOF-to-actuator bookkeeping, + stepping, reset, and the pre-clamp computed-effort buffer the + in-graph telemetry kernel reads on the post-actuator hook. + """ + + @dataclass(frozen=True) + class ArticulationBinding: + """Newton fast-path init state for one articulation. + + Returned by :meth:`bind_articulation`. Bundles the implicit-DOF mask + and the per-articulation view of the adapter's computed-effort buffer. + """ + + implicit_dof_mask: wp.array + """Per-DOF mask consumed by ``sync_torque_telemetry``; ``1`` on implicit-actuator DOFs, ``0`` otherwise.""" + + implicit_dof_mask_owner: torch.Tensor + """Torch tensor owning the memory :attr:`implicit_dof_mask` aliases; keep referenced for the mask's lifetime.""" + + computed_effort_view: wp.array + """This articulation's slice of the adapter's pre-clamp computed-effort buffer, ``(num_envs, num_joints)``.""" + + def __init__( + self, + actuators: list[Actuator], + num_envs: int, + num_joints: int, + dof_offset: int, + device: str, + ): + self.actuators = actuators + self.num_joints = num_joints + + self._num_envs = num_envs + self._dof_offset = dof_offset + self._device = device + + # Collect the set of local DOFs covered by some actuator. Only the + # env-0 slice of each actuator's flat ``indices`` array is needed — + # later envs are repeats with a constant ``num_joints`` stride. + managed: set[int] = set() + for act in actuators: + all_indices = act.indices.numpy() + num_per_act = len(all_indices) // num_envs + for global_dof in all_indices[:num_per_act]: + local_dof = global_dof - dof_offset + if 0 <= local_dof < num_joints: + managed.add(local_dof) + + if len(managed) == num_joints: + self.joint_indices: torch.Tensor | slice = slice(None) + else: + self.joint_indices = torch.tensor(sorted(managed), dtype=torch.int32, device=device) + + self._states_a = [act.state() for act in actuators] + self._states_b = [act.state() for act in actuators] + + # Pre-clamp computed effort buffer. Each Newton actuator scatter-adds + # its raw controller output to ``sim_control.joint_computed_f`` when + # ``control_computed_output_attr`` is set; we route that to this + # buffer so the post-actuator telemetry kernel can report the actual + # computed (pre-clamp) effort instead of mirroring ``joint_f``. The + # binding onto ``sim_control`` happens in :meth:`finalize`. + self._computed_effort = wp.zeros( + num_envs * num_joints, + dtype=wp.float32, + device=device, + ) + self.computed_effort_2d = self._computed_effort.reshape((num_envs, num_joints)) + for act in actuators: + act.control_computed_output_attr = "joint_computed_f" + + def finalize(self, sim_control: Any) -> None: + """Bind the pre-clamp computed-effort buffer onto ``sim_control``. + + Args: + sim_control: The ``sim_control`` object that will be passed + to :meth:`step` for this adapter's lifetime. Newton's + ``Control`` on the Newton backend, an + :class:`~isaaclab.actuators.newton.physx_wrapper.PhysxActuatorWrapper` + on the PhysX backend. + """ + sim_control.joint_computed_f = self._computed_effort + + def step(self, sim_state: Any, sim_control: Any, dt: float) -> None: + """Zero actuated DOFs, step all actuators, and swap state buffers. + + Args: + sim_state: Object with ``joint_q``, ``joint_qd``, etc. + Newton ``State`` on the Newton backend, + :class:`~isaaclab.actuators.newton.physx_wrapper.PhysxActuatorWrapper` + on the PhysX backend. + sim_control: Object with ``joint_f``, ``joint_target_q``, etc. + Newton ``Control`` on the Newton backend, + :class:`~isaaclab.actuators.newton.physx_wrapper.PhysxActuatorWrapper` + on the PhysX backend. + dt: Physics timestep [s]. + """ + # Zero before scatter-add (actuators accumulate into this buffer). + self._computed_effort.zero_() + for act in self.actuators: + wp.launch( + zero_at_indices_kernel, + dim=act.indices.shape[0], + inputs=[sim_control.joint_f, act.indices], + ) + for act, sa, sb in zip(self.actuators, self._states_a, self._states_b): + act.step(sim_state, sim_control, sa, sb, dt=dt) + self._swap_state_buffers() + + def _swap_state_buffers(self) -> None: + """Advance the actuator state ping-pong after an eager step or graph replay.""" + self._states_a, self._states_b = self._states_b, self._states_a + + def reset(self, env_ids: Sequence[int] | torch.Tensor | None = None) -> None: + """Reset actuator states for the given environments. + + Args: + env_ids: Environment indices to reset. ``None`` (or + ``slice(None)``, which IsaacLab callers sometimes pass) + resets all environments. Otherwise expects a torch tensor + or sequence of int indices. + + Newton's :meth:`Actuator.State.reset` expects a per-DOF boolean + mask of length ``num_actuators`` (= ``num_envs * dofs_per_actuator``), + not a per-env mask — each entry gates the corresponding column of + the actuator's state buffers (delay queue, controller integral, + etc.). We therefore build a per-actuator per-DOF mask from the + env mask before delegating to each state. + """ + if env_ids is None or env_ids == slice(None): + for sa, sb in zip(self._states_a, self._states_b): + if sa is not None: + sa.reset(None) + if sb is not None: + sb.reset(None) + return + + if isinstance(env_ids, torch.Tensor): + if env_ids.numel() == 0: + return + idx = wp.from_torch(env_ids.to(device=self._device).contiguous().to(torch.int32), dtype=wp.int32) + else: + if len(env_ids) == 0: + return + idx = wp.array(list(env_ids), dtype=wp.int32, device=self._device) + env_mask = wp.zeros(self._num_envs, dtype=wp.bool, device=self._device) + wp.launch(set_mask_kernel, dim=idx.shape[0], inputs=[env_mask, idx], device=self._device) + + for act, sa, sb in zip(self.actuators, self._states_a, self._states_b): + per_dof_mask = wp.zeros(act.indices.shape[0], dtype=wp.bool, device=self._device) + wp.launch( + build_per_dof_env_mask_kernel, + dim=act.indices.shape[0], + inputs=[act.indices, env_mask, self._dof_offset, self.num_joints, per_dof_mask], + device=self._device, + ) + if sa is not None: + sa.reset(per_dof_mask) + if sb is not None: + sb.reset(per_dof_mask) + + def bind_articulation( + self, + *, + implicit_joint_indices: Sequence[slice | torch.Tensor | None], + dof_offset: int, + num_joints: int, + ) -> ArticulationBinding: + """Assemble the Newton fast-path init state for one articulation. + + Builds the implicit-DOF mask and slices this adapter's + computed-effort buffer to the articulation's columns. + + Args: + implicit_joint_indices: Joint selectors of the articulation's implicit + actuator groups in public joint order; they define + :attr:`ArticulationBinding.implicit_dof_mask`. + dof_offset: Offset of this articulation's DOFs in the adapter's + env-major global index space (``0`` on PhysX, view-dependent + on Newton). + num_joints: Articulation-local joint count. Distinct from + :attr:`num_joints`, which is the whole-model per-env DOF + stride used to lay out the actuator index arrays. + + Returns: + The bundled :class:`ArticulationBinding` for this articulation. + """ + implicit_dof_mask, implicit_dof_mask_owner = build_implicit_dof_mask( + implicit_joint_indices, num_joints, self._device + ) + computed_effort_view = self.computed_effort_2d[:, dof_offset : dof_offset + num_joints] + return self.ArticulationBinding( + implicit_dof_mask=implicit_dof_mask, + implicit_dof_mask_owner=implicit_dof_mask_owner, + computed_effort_view=computed_effort_view, + ) + + @property + def is_all_graphable(self) -> bool: + """``True`` when all actuators are CUDA-graph-safe.""" + return len(self.actuators) > 0 and all(a.is_graphable() for a in self.actuators) + + @property + def is_stateful(self) -> bool: + """``True`` when any actuator maintains delay or controller state.""" + return any(a.is_stateful() for a in self.actuators) + + @classmethod + def from_usd( + cls, + stage: Any, + joint_names: list[str], + num_envs: int, + num_joints: int, + device: str, + articulation_prim_path: str | None = None, + ) -> NewtonActuatorAdapter: + """Build an adapter from ``NewtonActuator`` prims authored on *stage*. + + This is the host-adapter counterpart of Newton's + ``ModelBuilder.add_usd``. It reads the same prims and constructs matching + :class:`~newton.actuators.Actuator` objects. Structurally compatible + joints are merged into one actuator with per-DOF parameter arrays and + combined indices. Newton backends use ``model.actuators`` instead. + + On PhysX and OVPhysX, :paramref:`joint_names` is in this adapter's local + public order and defines the local indices assigned to parsed actuator targets. + + Args: + stage: USD stage containing ``NewtonActuator`` prims. + joint_names: All articulation joint names in adapter-local public order. + num_envs: Number of environments. + num_joints: Number of joints per environment. + device: Warp device string, for example ``"cuda:0"``. + articulation_prim_path: Root prim path of environment zero's + articulation. When set, only prims under this subtree are + considered; otherwise the whole stage is scanned. + + Returns: + Adapter whose actuator indices use :paramref:`joint_names` order. + + Raises: + ValueError: If no authored actuator targets a name in + :paramref:`joint_names`. + """ + actuators = _create_actuators_from_usd( + stage, + joint_names, + num_envs, + num_joints, + device, + articulation_prim_path=articulation_prim_path, + ) + return cls(actuators, num_envs, num_joints, dof_offset=0, device=device) + + +# --------------------------------------------------------------------------- +# Component-addressed parameter access via Newton's selection API. +# --------------------------------------------------------------------------- + + +def read_group_parameter(collection: ActuatorCollection, name: str, component: str, attr: str) -> torch.Tensor: + """Read one live Newton actuator parameter for a native group. + + Group-scoped, user-ordered reads of the controller-owned storage. For raw + component access, use the group's Newton actuator object (the collection + mapping entry) directly. + + Args: + collection: The articulation's actuator collection. + name: Actuator group name. + component: Component kind: ``"controller"``, ``"delay"``, or ``"clamping"``. + attr: Parameter name on that component (e.g. ``"kp"``, ``"max_effort"``). + + Returns: + Live values in the group's joint order, shape + ``(num_instances, group_num_joints)``, in the parameter's dtype. + Units follow the addressed parameter. + + Raises: + ValueError: If the group is not executed by Newton actuators, the + component name is unknown, or no actuator exposes the parameter. + """ + owners = _group_parameter_owners(collection, name, component, attr) + view = collection._newton_selection.view + values: torch.Tensor | None = None + for actuator, owner in owners: + # Non-driven DOFs read as zeros, and groups are disjoint, so overlaying is a sum. + projected = wp.to_torch(view.get_actuator_parameter(actuator, owner, attr)) + values = projected if values is None else values + projected + return values[:, collection._newton_group_columns(name)] + + +def write_group_parameter( + collection: ActuatorCollection, + name: str, + component: str, + attr: str, + values: torch.Tensor, + env_ids: torch.Tensor | None = None, + joint_ids: torch.Tensor | None = None, +) -> None: + """Write one Newton actuator parameter for a native group. + + Group-scoped, user-ordered writes that reach the controller-owned storage + through Newton's selection API. For raw component access, use the group's + Newton actuator object (the collection mapping entry) directly. + + Args: + collection: The articulation's actuator collection. + name: Actuator group name. + component: Component kind: ``"controller"``, ``"delay"``, or ``"clamping"``. + attr: Parameter name on that component (e.g. ``"kp"``, ``"max_effort"``). + values: New values, shape ``(len(env_ids), len(joint_ids))``. Units + follow the addressed parameter. + env_ids: Environment indices to update. Defaults to all environments. + joint_ids: Group-local joint indices to update. Defaults to all of + the group's joints. + + Raises: + ValueError: Same conditions as :func:`read_group_parameter`. + """ + owners = _group_parameter_owners(collection, name, component, attr) + view = collection._newton_selection.view + device = collection.device + columns = collection._newton_group_columns(name) + if joint_ids is not None: + columns = columns[joint_ids.to(device, dtype=torch.long)] + mask = None + env_rows: torch.Tensor | None = None + if env_ids is not None: + env_rows = env_ids.to(device, dtype=torch.long).unsqueeze(1) + mask_torch = torch.zeros(collection.num_instances, dtype=torch.bool, device=device) + mask_torch[env_rows] = True + mask = wp.from_torch(mask_torch, dtype=wp.bool) + values = values.to(device) + for actuator, owner in owners: + current = view.get_actuator_parameter(actuator, owner, attr) + current_torch = wp.to_torch(current) + if env_rows is None: + current_torch[:, columns] = values.to(dtype=current_torch.dtype) + else: + current_torch[env_rows, columns.unsqueeze(0)] = values.to(dtype=current_torch.dtype) + view.set_actuator_parameter(actuator=actuator, component=owner, name=attr, values=current, mask=mask) + + +def _group_parameter_owners( + collection: ActuatorCollection, name: str, component: str, attr: str +) -> list[tuple[Actuator, Any]]: + """Resolve the component instances exposing ``attr`` for one native group.""" + if name not in collection._groups: + raise KeyError(name) + if collection._newton_selection is None or name not in collection._native_group_names: + raise ValueError(f"Actuator group '{name}' is not executed by Newton actuators.") + group_actuators = collection._groups[name] + if not isinstance(group_actuators, tuple): + group_actuators = (group_actuators,) + owners = [ + (actuator, owner) + for actuator in group_actuators + if (owner := resolve_actuator_component(actuator, component, attr)) is not None + ] + if not owners: + raise ValueError(f"No Newton actuator exposes parameter ('{component}', '{attr}').") + return owners + + +def resolve_actuator_component(actuator: Actuator, component: str, attr: str) -> Any | None: + """Return the component instance that exposes ``attr`` on the addressed component kind. + + ``component`` selects the actuator's ``"controller"``, ``"delay"``, or + ``"clamping"`` entry; the returned object is what Newton's + :meth:`~newton.selection.ArticulationView.get_actuator_parameter` and + :meth:`~newton.selection.ArticulationView.set_actuator_parameter` take as + their ``component`` argument. Returns ``None`` when the component is absent + on this actuator or does not expose ``attr``. Raises ``ValueError`` on + unknown component names or ambiguous clamping matches. + """ + if component == "controller": + owner = actuator.controller + elif component == "delay": + owner = getattr(actuator, "delay", None) + elif component == "clamping": + matches = [entry for entry in (getattr(actuator, "clamping", None) or []) if hasattr(entry, attr)] + if len(matches) > 1: + names = ", ".join(type(entry).__name__ for entry in matches) + raise ValueError(f"Ambiguous clamping parameter '{attr}': exposed by {names}.") + owner = matches[0] if matches else None + else: + raise ValueError(f"Unknown actuator component '{component}'. Expected 'controller', 'delay', or 'clamping'.") + if owner is None or not hasattr(owner, attr): + return None + return owner + + +class LightArticulationView: + """Newton's actuator-parameter selection over bare actuators, without a Model. + + The PhysX-family backends build Newton actuators from USD without a Newton + :class:`~newton.Model`, so they cannot construct a real + :class:`~newton.selection.ArticulationView`. The view's actuator-parameter + section only consumes the placement attributes below, so this stand-in + provides them for the PhysX flat layout (one articulation per world, + identity joint order, per-world DOF stride equal to the joint count) and + borrows the real implementations unchanged. + """ + + def __init__(self, num_envs: int, num_joints: int, device: str): + self.world_count = num_envs + self.count_per_world = 1 + self.device = device + self.full_mask = wp.ones(num_envs, dtype=wp.bool, device=device) + self.frequency_layouts = { + Model.AttributeFrequency.JOINT_DOF: FrequencyLayout( + offset=0, + stride_between_worlds=num_joints, + stride_within_worlds=num_joints, + value_count=num_joints, + indices=list(range(num_joints)), + device=device, + ) + } + + # The real implementations, unchanged: they only read the attributes above. + get_actuator_parameter = ArticulationView.get_actuator_parameter + set_actuator_parameter = ArticulationView.set_actuator_parameter + _get_actuator_dof_mapping = ArticulationView._get_actuator_dof_mapping + _resolve_world_mask = ArticulationView._resolve_world_mask + + +@dataclass(frozen=True) +class NewtonActuatorSelection: + """Execution-setup handoff for Newton actuator parameter access. + + Pure data returned by + :meth:`~isaaclab.actuators.ActuatorControl.finalize_native_actuators` and + consumed by the group-scoped parameter access functions + (:func:`read_group_parameter` / :func:`write_group_parameter`) and by the + collection when it maps native groups to their Newton actuator objects. + """ + + view: Any + """Newton :class:`~newton.selection.ArticulationView` or :class:`LightArticulationView` over the articulation.""" + + actuators: list[Actuator] + """Newton actuators visible to the view.""" + + joint_user_to_backend_indices: tuple[int, ...] | None = None + """Optional public-to-backend joint permutation for the view's DOF columns.""" + + +# --------------------------------------------------------------------------- +# PhysX-only USD parsing +# --------------------------------------------------------------------------- + +_ResolvedComponent: TypeAlias = tuple[type, dict[str, Any]] +_ResolvedActuatorSpec: TypeAlias = tuple[int, type, dict[str, Any], list[_ResolvedComponent]] + + +def _actuator_signature( + controller_class: type, + controller_arguments: dict[str, Any], + component_arguments: list[_ResolvedComponent], +) -> tuple: + """Build Newton's structural grouping key for a parsed actuator spec.""" + + def make_hashable(value: Any) -> Any: + if isinstance(value, list | tuple): + return tuple(make_hashable(item) for item in value) + return value + + def shared_key(component_class: type, resolved: dict[str, Any]) -> tuple: + shared_names = getattr(component_class, "SHARED_PARAMS", set()) + return tuple(sorted((name, make_hashable(resolved[name])) for name in shared_names if name in resolved)) + + clamping_key: list[tuple] = [] + has_delay = False + for comp_cls, resolved in component_arguments: + if issubclass(comp_cls, Delay): + has_delay = True + elif issubclass(comp_cls, Clamping): + clamping_key.append((comp_cls, shared_key(comp_cls, resolved))) + + return (controller_class, has_delay, tuple(clamping_key), shared_key(controller_class, controller_arguments)) + + +def _tile_per_dof_arguments( + arguments: list[dict[str, Any]], + num_envs: int, + dtype: type, + device: wp.Device, +) -> dict[str, wp.array]: + """Pack per-joint scalar arguments in environment-major order.""" + if not arguments: + return {} + + numpy_dtype = np.int32 if dtype == wp.int32 else np.float32 + return { + name: wp.array( + np.tile(np.asarray([per_joint[name] for per_joint in arguments], dtype=numpy_dtype), num_envs), + dtype=dtype, + device=device, + ) + for name in arguments[0] + } + + +def _create_actuators_from_usd( + stage: Any, + joint_names: list[str], + num_envs: int, + num_total_joints: int, + device: str, + articulation_prim_path: str | None = None, +) -> list[Actuator]: + """Parse ``NewtonActuator`` prims and instantiate standalone actuators. + + This mirrors the actuator construction that Newton's + ``ModelBuilder.add_usd`` performs, but operates independently of a + Newton ``Model``. It is used on the PhysX backend where there is no + Newton simulation — actuators are stepped manually via the adapter. + + Because PhysX articulations have no free or ball joints, every + joint's coordinate count equals its DOF count. A single + ``indices`` array is therefore sufficient for all index roles + (``indices``, ``pos_indices``, ``target_pos_indices``). + + Joints with the same controller and clamping structure are merged into + one :class:`Actuator`. Scalar parameters (``kp``, ``kd``, + ``saturation_effort``, delay, etc.) are packed per DOF. Parameters marked + as ``SHARED_PARAMS`` (e.g. ``model_path``, ``lookup_positions``) remain + part of the grouping key and are passed through directly. + """ + from collections import defaultdict # noqa: PLC0415 + + from newton.actuators import parse_actuator_prim # noqa: PLC0415 + + from pxr import Usd # noqa: PLC0415 + + wp_device = wp.get_device(device) + + joint_name_to_idx: dict[str, int] = {name: i for i, name in enumerate(joint_names)} + + if articulation_prim_path is not None: + root_prim = stage.GetPrimAtPath(articulation_prim_path) + else: + root_prim = stage.GetPseudoRoot() + + parsed_per_joint: dict[int, Any] = {} + for prim in Usd.PrimRange(root_prim): + parsed = parse_actuator_prim(prim) + if parsed is None: + continue + target_name = parsed.target_path.rsplit("/", 1)[-1] + if target_name in joint_name_to_idx: + parsed_per_joint[joint_name_to_idx[target_name]] = parsed + + if not parsed_per_joint: + raise ValueError(f"No NewtonActuator prims found targeting any of: {joint_names}") + + groups: dict[tuple, list[_ResolvedActuatorSpec]] = defaultdict(list) + for local_idx, parsed in sorted(parsed_per_joint.items()): + controller_arguments = parsed.controller_class.resolve_arguments(dict(parsed.controller_kwargs)) + component_arguments = [ + (comp_cls, comp_cls.resolve_arguments(comp_kwargs)) for comp_cls, comp_kwargs in parsed.component_specs + ] + sig = _actuator_signature(parsed.controller_class, controller_arguments, component_arguments) + groups[sig].append((local_idx, parsed.controller_class, controller_arguments, component_arguments)) + + actuators = [] + for grouped_specs in groups.values(): + local_indices = [spec[0] for spec in grouped_specs] + controller_class = grouped_specs[0][1] + resolved_controllers = [spec[2] for spec in grouped_specs] + resolved_components = [spec[3] for spec in grouped_specs] + + flat_indices = np.array( + [idx + e * num_total_joints for e in range(num_envs) for idx in local_indices], + dtype=np.uint32, + ) + indices = wp.array(flat_indices, device=wp_device) + + # Controller + shared_ctrl = getattr(controller_class, "SHARED_PARAMS", set()) + ctrl_arguments = [ + {key: value for key, value in resolved.items() if key not in shared_ctrl} + for resolved in resolved_controllers + ] + ctrl_shared = {key: value for key, value in resolved_controllers[0].items() if key in shared_ctrl} + controller = controller_class( + **_tile_per_dof_arguments(ctrl_arguments, num_envs, wp.float32, wp_device), + **ctrl_shared, + ) + + # Components (delay + clampings) + clamping_components = [ + [(comp_cls, resolved) for comp_cls, resolved in components if issubclass(comp_cls, Clamping)] + for components in resolved_components + ] + delay_arguments = [ + resolved + for components in resolved_components + for comp_cls, resolved in components + if issubclass(comp_cls, Delay) + ] + + delay = None + if delay_arguments: + max_delay = max(int(arguments["delay_steps"]) for arguments in delay_arguments) + if max_delay > 0: + delay = Delay( + **_tile_per_dof_arguments(delay_arguments, num_envs, wp.int32, wp_device), + max_delay=max_delay, + ) + + clampings = [] + for component_index, (comp_cls, _) in enumerate(clamping_components[0]): + resolved_clampings = [components[component_index][1] for components in clamping_components] + shared_clamp = getattr(comp_cls, "SHARED_PARAMS", set()) + clamp_arguments = [ + {key: value for key, value in resolved.items() if key not in shared_clamp} + for resolved in resolved_clampings + ] + clamp_shared = {key: value for key, value in resolved_clampings[0].items() if key in shared_clamp} + clampings.append( + comp_cls( + **_tile_per_dof_arguments(clamp_arguments, num_envs, wp.float32, wp_device), + **clamp_shared, + ) + ) + + actuator = Actuator( + indices=indices, + controller=controller, + delay=delay, + clamping=clampings if clampings else None, + control_target_pos_attr="joint_target_pos", + control_target_vel_attr="joint_target_vel", + ) + actuators.append(actuator) + + return actuators diff --git a/source/isaaclab_newton/isaaclab_newton/actuators/kernels.py b/source/isaaclab/isaaclab/actuators/newton/kernels.py similarity index 51% rename from source/isaaclab_newton/isaaclab_newton/actuators/kernels.py rename to source/isaaclab/isaaclab/actuators/newton/kernels.py index d4c4ef1b91c8..944c88d0f4bb 100644 --- a/source/isaaclab_newton/isaaclab_newton/actuators/kernels.py +++ b/source/isaaclab/isaaclab/actuators/newton/kernels.py @@ -5,18 +5,15 @@ """Shared Warp kernels for the Newton actuator fast path.""" +from collections.abc import Sequence + import torch import warp as wp -from isaaclab.actuators import ActuatorBase, ImplicitActuator - # --------------------------------------------------------------------------- # Adapter / per-actuator helper kernels: per-DOF zeroing, env-mask building, -# per-DOF env-mask projection (used by :meth:`NewtonActuatorAdapter.reset`), -# and a partial scatter for DR gain updates that overwrites only the cells -# in a (env_ids × joint_ids) sub-grid of a Newton ``Actuator``'s controller -# parameter array. Used on the PhysX backend (no Newton view available); -# the Newton backend uses ``ArticulationView.set_actuator_parameter`` instead. +# and per-DOF env-mask projection (used by :meth:`NewtonActuatorAdapter.reset`). +# Parameter reads and writes go through Newton's selection API instead. # --------------------------------------------------------------------------- @@ -56,100 +53,6 @@ def build_per_dof_env_mask_kernel( out_mask[i] = env_mask[env] -@wp.kernel(enable_backward=False) -def scatter_gain_kernel( - src: wp.array(dtype=wp.float32), - dst: wp.array(dtype=wp.float32), - indices: wp.array(dtype=wp.uint32), - dof_offset: int, - num_joints: int, - env_stride: int, -): - """Scatter per-actuator ``src`` values into a flat per-env-per-DOF ``dst``. - - Used at adapter finalize to snapshot each ``controller.kp`` / - ``controller.kd`` into the ``(num_envs, num_joints)`` torch tensor - that ``randomize_actuator_gains`` reads as - ``actuator.stiffness`` / ``.damping`` for its - ``default_joint_stiffness`` / ``default_joint_damping`` baseline. - - The actuator's ``indices`` are global DOF ids laid out env-major with a - per-env stride of ``env_stride`` — the *whole model's* per-env DOF count, - which on a floating-base articulation exceeds ``num_joints`` (the - articulation-local, actuated joint count) by the free-root DOFs. The env - index must therefore be decoded with ``env_stride``, not ``num_joints``; - the articulation-local joint offset is what remains after removing the - env's block and lands in ``[0, num_joints)`` because ``indices`` only ever - holds this articulation's joints. - - Args: - src: Per-actuator parameter values (e.g. ``controller.kp``). - dst: Flat ``(num_envs * num_joints)`` articulation-local snapshot buffer. - indices: Actuator's flat env-major global DOF indices. - dof_offset: Offset of this articulation's DOFs in the env-major - global index space (``0`` on PhysX, view-dependent on Newton). - num_joints: Articulation-local joint count (``dst``'s inner stride). - env_stride: Whole-model per-env DOF count (the stride used to build - ``indices``). - """ - i = wp.tid() - global_dof = int(indices[i]) - dof_offset - env = global_dof // env_stride - local_dof = global_dof - env * env_stride - dst[env * num_joints + local_dof] = src[i] - - -@wp.kernel(enable_backward=False) -def patch_actuator_param_kernel( - indices: wp.array(dtype=wp.uint32), - env_id_pos: wp.array(dtype=wp.int32), - joint_id_pos: wp.array(dtype=wp.int32), - values: wp.array2d(dtype=wp.float32), - dof_offset: int, - num_joints: int, - dst: wp.array(dtype=wp.float32), -): - """Per-actuator scatter for partial DR gain updates. - - For each slot ``i`` in the actuator's flat env-major ``indices``, derive - the (env, local-joint) pair, look it up against the dense position - arrays, and — when both axes are in the DR sub-grid — overwrite - ``dst[i]`` (the controller parameter) with ``values[e_pos, j_pos]``. - Cells outside the sub-grid are left untouched. - - Note: - This kernel is PhysX-only (the Newton backend patches gains via - :meth:`ArticulationView.set_actuator_parameter`). On PhysX every - joint's coordinate count equals its DOF count, so the per-env stride - used to build ``indices`` equals ``num_joints`` and the ``env`` / - ``joint`` split below is exact. Do not reuse this kernel on a layout - whose per-env DOF stride exceeds ``num_joints`` (e.g. a floating-base - Newton model) without threading the true stride, or the ``joint`` - split will alias across envs — see :func:`scatter_gain_kernel`. - - Args: - indices: Actuator's flat indices into the (env-major) DOF layout. - env_id_pos: ``env_id_pos[env]`` gives the row in ``values`` for - envs being updated, ``-1`` otherwise. Length ``num_envs``. - joint_id_pos: ``joint_id_pos[joint]`` gives the column in - ``values`` for joints being updated, ``-1`` otherwise. - Length ``num_joints`` (articulation-local). - values: New parameter values shaped ``(len(env_ids), len(joint_ids))``. - dof_offset: Offset of this articulation's DOFs in the env-major - global index space (``0`` on PhysX, view-dependent on Newton). - num_joints: Articulation-local joint count. - dst: Per-actuator controller parameter array (e.g. ``controller.kp``). - """ - i = wp.tid() - global_dof = int(indices[i]) - dof_offset - env = global_dof // num_joints - joint = global_dof % num_joints - e_pos = env_id_pos[env] - j_pos = joint_id_pos[joint] - if e_pos >= 0 and j_pos >= 0: - dst[i] = values[e_pos, j_pos] - - # --------------------------------------------------------------------------- # Articulation-level kernels: in-graph post-actuator hook. # --------------------------------------------------------------------------- @@ -206,14 +109,20 @@ def sync_torque_telemetry( def build_implicit_dof_mask( - actuators: dict[str, ActuatorBase], + implicit_joint_indices: "Sequence[slice | torch.Tensor | None]", num_joints: int, device: str, ) -> tuple[wp.array, torch.Tensor]: """Per-DOF mask consumed by :func:`sync_torque_telemetry`. - Entry is ``1`` for DOFs covered by an - :class:`~isaaclab.actuators.ImplicitActuator` group, ``0`` otherwise. + Entry is ``1`` for DOFs covered by an implicit actuator group, ``0`` otherwise. + + Args: + implicit_joint_indices: Joint selectors of the implicit groups, e.g. from + :meth:`ActuatorCollection._implicit_group_joint_indices`. ``slice`` or + ``None`` selects all joints. + num_joints: Articulation joint count. + device: Torch/Warp device string. Returns: Tuple of ``(wp_mask, torch_owner)``. ``wp_mask`` is the Warp @@ -226,10 +135,7 @@ def build_implicit_dof_mask( ``wp_mask``'s device pointer will read garbage at replay time. """ modes = torch.zeros(num_joints, dtype=torch.int32, device=device) - for actuator in actuators.values(): - if not isinstance(actuator, ImplicitActuator): - continue - j_ids = actuator.joint_indices + for j_ids in implicit_joint_indices: if isinstance(j_ids, slice) or j_ids is None: modes[:] = 1 else: diff --git a/source/isaaclab/isaaclab/actuators/newton/physx_runtime.py b/source/isaaclab/isaaclab/actuators/newton/physx_runtime.py new file mode 100644 index 000000000000..4765d202ce6c --- /dev/null +++ b/source/isaaclab/isaaclab/actuators/newton/physx_runtime.py @@ -0,0 +1,181 @@ +# 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 + +"""Shared host runtime for Newton-native actuator execution.""" + +from __future__ import annotations + +import logging +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +import warp as wp + +from .adapter import NewtonActuatorAdapter, build_implicit_dof_mask +from .physx_wrapper import PhysxActuatorWrapper + +if TYPE_CHECKING: + from isaaclab.actuators import ActuatorCollection + + +class PhysxActuatorRuntime: + """Step Newton actuators on a PhysX-family backend. + + PhysX does not host Newton actuators natively, so this runtime builds the + execution environment around them: it constructs the adapter from the + authored USD prims, binds a :class:`~isaaclab.actuators.newton.physx_wrapper.PhysxActuatorWrapper` + that impersonates Newton's ``State``/``Control`` over pointer-stable PhysX + and command buffers, and steps the actuators each physics step, eagerly or + through captured CUDA graphs. Shared by the PhysX and OVPhysX backends. + """ + + def __init__(self, articulation: Any, *, logger: logging.Logger): + self._articulation = articulation + self._logger = logger + self.wrapper: PhysxActuatorWrapper | None = None + self.adapter: NewtonActuatorAdapter | None = None + self.binding: NewtonActuatorAdapter.ArticulationBinding | None = None + self.native_actuator_graphs: tuple[wp.Graph, wp.Graph] | None = None + self._native_actuator_graph_index = 0 + + def prepare( + self, + collection: ActuatorCollection, + *, + stage: Any, + articulation_prim_path: str | None, + adapt_usd_actuators: bool = True, + ) -> None: + """Create and bind the adapter and its pointer-stable host wrapper. + + Args: + collection: Actuator collection under construction. + stage: USD stage holding the authored actuator prims. + articulation_prim_path: Prim path of the articulation root. + adapt_usd_actuators: Whether to adapt authored ``NewtonActuator`` prims. + Implicit-only articulations author none: the solver applies their + drives, and the wrapper alone backs the telemetry fast path. + """ + articulation = self._articulation + self.wrapper = PhysxActuatorWrapper.create( + num_envs=articulation.num_instances, + num_joints=articulation.num_joints, + device=articulation.device, + ) + self.wrapper.joint_q = articulation._data.joint_pos.warp.reshape(-1) + self.wrapper.joint_qd = articulation._data.joint_vel.warp.reshape(-1) + self.wrapper.joint_target_q = collection.target_command.position.warp.reshape(-1) + self.wrapper.joint_target_qd = collection.target_command.velocity.warp.reshape(-1) + self.wrapper.joint_target_pos = self.wrapper.joint_target_q + self.wrapper.joint_target_vel = self.wrapper.joint_target_qd + self.wrapper.joint_act = collection.target_command.effort.warp.reshape(-1) + if not adapt_usd_actuators: + return + self.adapter = NewtonActuatorAdapter.from_usd( + stage=stage, + joint_names=articulation.joint_names, + num_envs=articulation.num_instances, + num_joints=articulation.num_joints, + device=articulation.device, + articulation_prim_path=articulation_prim_path, + ) + self.adapter.finalize(self.wrapper) + + def finalize(self, collection: ActuatorCollection) -> None: + """Bind telemetry and native gain defaults after collection construction.""" + articulation = self._articulation + if self.adapter is None: + articulation._implicit_dof_mask, articulation._implicit_dof_mask_owner = build_implicit_dof_mask( + collection._implicit_group_joint_indices(), articulation.num_joints, articulation.device + ) + articulation._data._sim_bind_joint_computed_effort = wp.zeros( + (articulation.num_instances, articulation.num_joints), + dtype=wp.float32, + device=articulation.device, + ) + return + + self.binding = self.adapter.bind_articulation( + implicit_joint_indices=collection._implicit_group_joint_indices(), + dof_offset=0, + num_joints=articulation.num_joints, + ) + articulation._implicit_dof_mask = self.binding.implicit_dof_mask + articulation._implicit_dof_mask_owner = self.binding.implicit_dof_mask_owner + articulation._data._sim_bind_joint_computed_effort = self.binding.computed_effort_view + + def compute(self, collection: ActuatorCollection, dt: float) -> None: + """Compute native efforts and synchronize telemetry for one physics step.""" + if self.adapter is not None: + device = wp.get_device(self._articulation.device) + if device.is_cuda and device.is_capturing and self.adapter.is_stateful: + raise RuntimeError( + "stateful Newton actuators cannot run inside an outer CUDA graph capture; " + "let the host actuator runtime manage their alternating graphs" + ) + if self.adapter.is_all_graphable and device.is_cuda and not device.is_capturing: + if self.native_actuator_graphs is None: + self._capture_native_actuator_graphs(collection, dt) + if self.native_actuator_graphs: + wp.capture_launch(self.native_actuator_graphs[self._native_actuator_graph_index]) + self.adapter._swap_state_buffers() + self._native_actuator_graph_index ^= 1 + return + self._run_native_actuator_kernels(collection, dt) + + def reset(self, env_ids: Sequence[int] | slice) -> None: + """Reset selected native actuator state.""" + if self.adapter is not None: + self.adapter.reset(env_ids) + + def _run_native_actuator_kernels(self, collection: ActuatorCollection, dt: float) -> None: + from . import kernels as actuator_kernels # noqa: PLC0415 + + articulation = self._articulation + if self.wrapper is None: + raise RuntimeError("Newton-native actuator wrapper was not initialized") + self.wrapper.joint_f_2d.assign(collection._joint_effort_target) + if self.adapter is not None: + self.adapter.step(self.wrapper, self.wrapper, dt) + wp.launch( + actuator_kernels.sync_torque_telemetry, + dim=(articulation.num_instances, articulation.num_joints), + inputs=[ + articulation._data.joint_pos.warp, + articulation._data.joint_vel.warp, + collection._joint_pos_target, + collection._joint_vel_target, + articulation._data.joint_stiffness.warp, + articulation._data.joint_damping.warp, + articulation._data.joint_effort_limits.warp, + articulation._implicit_dof_mask, + self.wrapper.joint_f_2d, + articulation._data._sim_bind_joint_computed_effort, + articulation._ALL_JOINT_INDICES, + False, + ], + outputs=[collection._computed_effort, collection._applied_effort], + device=articulation.device, + ) + + def _capture_native_actuator_graphs(self, collection: ActuatorCollection, dt: float) -> None: + if self.adapter is None: + return + states_a = self.adapter._states_a + states_b = self.adapter._states_b + graphs = [] + try: + for _ in range(2): + with wp.ScopedCapture(device=self._articulation.device, force_module_load=True) as capture: + self._run_native_actuator_kernels(collection, dt) + graphs.append(capture.graph) + except Exception as exc: + self._logger.warning("Host Newton-actuator CUDA graph capture failed; using eager execution: %s", exc) + graphs = [] + finally: + self.adapter._states_a = states_a + self.adapter._states_b = states_b + self.native_actuator_graphs = tuple(graphs) if graphs else () + self._native_actuator_graph_index = 0 diff --git a/source/isaaclab_newton/isaaclab_newton/actuators/physx_wrapper.py b/source/isaaclab/isaaclab/actuators/newton/physx_wrapper.py similarity index 81% rename from source/isaaclab_newton/isaaclab_newton/actuators/physx_wrapper.py rename to source/isaaclab/isaaclab/actuators/newton/physx_wrapper.py index 8c788be408ae..11a4bf2fe3a5 100644 --- a/source/isaaclab_newton/isaaclab_newton/actuators/physx_wrapper.py +++ b/source/isaaclab/isaaclab/actuators/newton/physx_wrapper.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""PhysX-only stepping helper for :class:`~newton.actuators.Actuator`. +"""Host-PhysX stepping helper for :class:`~newton.actuators.Actuator`. Newton's :meth:`Actuator.step` requires a ``sim_state`` / ``sim_control`` pair that exposes flat 1-D Warp arrays (``joint_q``, ``joint_qd``, @@ -18,11 +18,11 @@ articulation code that calls :meth:`Actuator.step` lives in ``newton_manager.py`` and has direct access to the model's state. -On the **PhysX backend**, no Newton solver exists — the actuators are -stepped manually from the Lab articulation's ``write_data_to_sim`` -path. Isaac Lab stores joint data as 2-D tensors (``num_envs × -num_joints``), so :class:`PhysxActuatorWrapper` provides zero-copy flat -views that satisfy the protocol without allocating new memory. +On a **host-PhysX backend** (PhysX or OVPhysX), no Newton solver exists — +the actuators are stepped manually from the Lab articulation's +``write_data_to_sim`` path. Isaac Lab stores joint data as 2-D tensors +(``num_envs × num_joints``), so :class:`PhysxActuatorWrapper` provides +zero-copy flat views that satisfy the protocol without allocating new memory. """ from __future__ import annotations @@ -35,7 +35,7 @@ @dataclass class PhysxActuatorWrapper: """Flat-array wrapper serving as ``sim_state`` / ``sim_control`` for - :meth:`Actuator.step` on the PhysX backend. + :meth:`Actuator.step` on host-PhysX backends. Most attributes are bound once at articulation init to zero-copy flat views of Isaac Lab's 2-D buffers. ``joint_f_2d`` is the only persistent diff --git a/source/isaaclab/isaaclab/assets/articulation/articulation_cfg.py b/source/isaaclab/isaaclab/assets/articulation/articulation_cfg.py index d7b6b1707eb4..ce0c6154343f 100644 --- a/source/isaaclab/isaaclab/assets/articulation/articulation_cfg.py +++ b/source/isaaclab/isaaclab/assets/articulation/articulation_cfg.py @@ -102,7 +102,10 @@ class InitialStateCfg(AssetBaseCfg.InitialStateCfg): """ actuators: dict[str, ActuatorBaseCfg] = MISSING - """Actuators for the robot with corresponding joint names.""" + """Actuators for the robot with corresponding joint names. + + Each joint can belong to at most one actuator group. + """ actuator_value_resolution_debug_print = False """Print the resolution of actuator final value when input cfg is different from USD value, Defaults to False diff --git a/source/isaaclab/isaaclab/assets/articulation/base_articulation.py b/source/isaaclab/isaaclab/assets/articulation/base_articulation.py index 5737e080fc08..f9b5b9d3fd0a 100644 --- a/source/isaaclab/isaaclab/assets/articulation/base_articulation.py +++ b/source/isaaclab/isaaclab/assets/articulation/base_articulation.py @@ -27,6 +27,7 @@ from .ordering_resolvers import _resolve_articulation_ordering_names if TYPE_CHECKING: + from isaaclab.actuators import ActuatorCollection from isaaclab.utils.wrench_composer import WrenchComposer from .articulation_cfg import ArticulationCfg @@ -35,6 +36,18 @@ logger = logging.getLogger(__name__) +def _as_bool_mask(mask: torch.Tensor | wp.array | None) -> wp.array | None: + """Coerce legacy nonzero-selectable masks to the ``wp.bool`` masks the command API expects. + + Deprecated with the ``set_joint_*_target_mask`` forwarders that consume it; + remove together in 4.0. + """ + if mask is None or (isinstance(mask, wp.array) and mask.dtype == wp.bool): + return mask + mask_torch = wp.to_torch(mask) if isinstance(mask, wp.array) else mask + return wp.from_torch((mask_torch != 0).contiguous(), dtype=wp.bool) + + class BaseArticulation(AssetBase): """An articulation asset class. @@ -105,12 +118,13 @@ class BaseArticulation(AssetBase): solver-view order matches. """ - actuators: dict - """Dictionary of actuator instances for the articulation. + actuators: ActuatorCollection + """Runtime actuator collection for the articulation. - The keys are the actuator names and the values are the actuator instances. The actuator instances - are initialized based on the actuator configurations specified in the :attr:`ArticulationCfg.actuators` - attribute. They are used to compute the joint commands during the :meth:`write_data_to_sim` function. + This read-only mapping exposes configured groups and owns articulation-wide + actuator commands and telemetry. Configure membership through + :attr:`ArticulationCfg.actuators` before construction; set runtime commands + through ``articulation.actuators.target_command``. """ def __init__(self, cfg: ArticulationCfg): @@ -1407,6 +1421,8 @@ def write_joint_friction_coefficient_to_sim_index( self, *, joint_friction_coeff: torch.Tensor | float | wp.array, + joint_dynamic_friction_coeff: torch.Tensor | float | wp.array | None = None, + joint_viscous_friction_coeff: torch.Tensor | float | wp.array | None = None, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: @@ -1426,6 +1442,11 @@ def write_joint_friction_coefficient_to_sim_index( Args: joint_friction_coeff: Backend-specific joint friction values. Shape is (len(env_ids), len(joint_ids)). + joint_dynamic_friction_coeff: Backend-specific dynamic friction values with the same shape. + If None, the dynamic component is not updated. Backends without a dynamic friction + property warn on nonzero values. + joint_viscous_friction_coeff: Backend-specific viscous friction values with the same shape. + If None, the viscous component is not updated. joint_ids: The joint indices to set the joint torque limits for. Defaults to None (all joints). env_ids: The environment indices to set the joint torque limits for. Defaults to None (all instances). """ @@ -1611,173 +1632,280 @@ def set_inertias_mask( """ raise NotImplementedError() - @abstractmethod @leapp_tensor_semantics(kind=OutputKindEnum.JOINT_POSITION, element_names_resolver=joint_names_resolver) def set_joint_position_target_index( self, *, - target: torch.Tensor | wp.array, + target: torch.Tensor | wp.array(dtype=wp.float32), joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, + full_data: bool = False, ) -> None: """Set joint position targets into internal buffers. - .. note:: - This method expects partial data. + .. deprecated:: + Use :attr:`isaaclab.actuators.ActuatorCollection.command` and call + ``set_position_index``. - .. tip:: - For maximum performance we recommend looking at the actual implementation of the method in the backend. - Some backends may provide optimized implementations for masks / indices. + .. note:: + This method accepts partial or full data. This function does not apply the joint targets to the simulation. It only fills the buffers with the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. Args: - target: Joint position targets. Shape is (len(env_ids), len(joint_ids)). + target: Joint position targets [m or rad, depending on joint type]. Shape is + ``(len(env_ids), len(joint_ids))``, or ``(num_instances, num_joints)`` when + :paramref:`full_data` is true. joint_ids: The joint indices to set the targets for. Defaults to None (all joints). env_ids: The environment indices to set the targets for. Defaults to None (all instances). + full_data: Whether :paramref:`target` contains all articulation joints and instances. """ - raise NotImplementedError() + warnings.warn( + "Articulation.set_joint_position_target_index is deprecated. Use " + "articulation.actuators.target_command.set_position_index instead.", + DeprecationWarning, + stacklevel=2, + ) + self.actuators.target_command.set_position_index( + value=target, joint_ids=joint_ids, env_ids=env_ids, full_data=full_data + ) + + def _write_deprecated_native_actuator_gain( + self, + writer_name: Literal["write_actuator_stiffness_to_sim", "write_actuator_damping_to_sim"], + gain_name: Literal["kp", "kd"], + values: torch.Tensor, + env_ids: torch.Tensor, + joint_ids: torch.Tensor, + ) -> None: + """Warn and forward a legacy native-controller gain write.""" + from isaaclab.actuators.newton import write_group_parameter # noqa: PLC0415 + + warnings.warn( + f"{writer_name} is deprecated in 3.x and will be removed in 4.0. Use " + "randomize_actuator_gains for managed randomization or " + "isaaclab.actuators.newton.write_group_parameter for direct controller writes.", + DeprecationWarning, + stacklevel=3, + ) + # Split the articulation-level joint selection into per-group columns. + joint_ids_long = joint_ids.to(self.device, dtype=torch.long) + for group_name in getattr(self.actuators, "_native_group_names", ()): + group_joints = self.actuators._group_joint_indices[group_name] + if isinstance(group_joints, slice): + in_group = torch.ones_like(joint_ids_long, dtype=torch.bool) + group_columns = joint_ids_long + else: + column_of_joint = torch.full((self.num_joints,), -1, dtype=torch.long, device=self.device) + group_joints = group_joints.to(self.device, dtype=torch.long) + column_of_joint[group_joints] = torch.arange(group_joints.numel(), device=self.device) + columns = column_of_joint[joint_ids_long] + in_group = columns >= 0 + group_columns = columns[in_group] + if not bool(in_group.any()): + continue + write_group_parameter( + self.actuators, + group_name, + "controller", + gain_name, + values[:, in_group], + env_ids=env_ids, + joint_ids=group_columns, + ) - @abstractmethod @leapp_tensor_semantics(kind=OutputKindEnum.JOINT_POSITION, element_names_resolver=joint_names_resolver) def set_joint_position_target_mask( self, *, - target: torch.Tensor | wp.array, - joint_mask: wp.array | None = None, - env_mask: wp.array | None = None, + target: torch.Tensor | wp.array(dtype=wp.float32), + joint_mask: wp.array(dtype=wp.bool) | None = None, + env_mask: wp.array(dtype=wp.bool) | None = None, ) -> None: """Set joint position targets into internal buffers. + .. deprecated:: + Use :attr:`isaaclab.actuators.ActuatorCollection.command` and call + ``set_position_mask``. + .. note:: This method expects full data. - .. tip:: - For maximum performance we recommend looking at the actual implementation of the method in the backend. - Some backends may provide optimized implementations for masks / indices. - This function does not apply the joint targets to the simulation. It only fills the buffers with the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. Args: - target: Joint position targets. Shape is (num_instances, num_joints). + target: Joint position targets [m or rad, depending on joint type]. Shape is + ``(num_instances, num_joints)``. joint_mask: Joint mask. If None, then all the joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). """ - raise NotImplementedError() + warnings.warn( + "Articulation.set_joint_position_target_mask is deprecated. Use " + "articulation.actuators.target_command.set_position_mask instead.", + DeprecationWarning, + stacklevel=2, + ) + self.actuators.target_command.set_position_mask( + value=target, + joint_mask=_as_bool_mask(joint_mask), + env_mask=_as_bool_mask(env_mask), + ) - @abstractmethod @leapp_tensor_semantics(kind=OutputKindEnum.JOINT_VELOCITY, element_names_resolver=joint_names_resolver) def set_joint_velocity_target_index( self, *, - target: torch.Tensor | wp.array, + target: torch.Tensor | wp.array(dtype=wp.float32), joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, + full_data: bool = False, ) -> None: """Set joint velocity targets into internal buffers. - .. note:: - This method expects partial data. + .. deprecated:: + Use :attr:`isaaclab.actuators.ActuatorCollection.command` and call + ``set_velocity_index``. - .. tip:: - For maximum performance we recommend looking at the actual implementation of the method in the backend. - Some backends may provide optimized implementations for masks / indices. + .. note:: + This method accepts partial or full data. This function does not apply the joint targets to the simulation. It only fills the buffers with the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. Args: - target: Joint velocity targets. Shape is (len(env_ids), len(joint_ids)). + target: Joint velocity targets [m/s or rad/s, depending on joint type]. Shape is + ``(len(env_ids), len(joint_ids))``, or ``(num_instances, num_joints)`` when + :paramref:`full_data` is true. joint_ids: The joint indices to set the targets for. Defaults to None (all joints). env_ids: The environment indices to set the targets for. Defaults to None (all instances). + full_data: Whether :paramref:`target` contains all articulation joints and instances. """ - raise NotImplementedError() + warnings.warn( + "Articulation.set_joint_velocity_target_index is deprecated. Use " + "articulation.actuators.target_command.set_velocity_index instead.", + DeprecationWarning, + stacklevel=2, + ) + self.actuators.target_command.set_velocity_index( + value=target, joint_ids=joint_ids, env_ids=env_ids, full_data=full_data + ) - @abstractmethod @leapp_tensor_semantics(kind=OutputKindEnum.JOINT_VELOCITY, element_names_resolver=joint_names_resolver) def set_joint_velocity_target_mask( self, *, - target: torch.Tensor | wp.array, - joint_mask: wp.array | None = None, - env_mask: wp.array | None = None, + target: torch.Tensor | wp.array(dtype=wp.float32), + joint_mask: wp.array(dtype=wp.bool) | None = None, + env_mask: wp.array(dtype=wp.bool) | None = None, ) -> None: """Set joint velocity targets into internal buffers. + .. deprecated:: + Use :attr:`isaaclab.actuators.ActuatorCollection.command` and call + ``set_velocity_mask``. + .. note:: This method expects full data. - .. tip:: - For maximum performance we recommend looking at the actual implementation of the method in the backend. - Some backends may provide optimized implementations for masks / indices. - This function does not apply the joint targets to the simulation. It only fills the buffers with the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. Args: - target: Joint velocity targets. Shape is (num_instances, num_joints). + target: Joint velocity targets [m/s or rad/s, depending on joint type]. Shape is + ``(num_instances, num_joints)``. joint_mask: Joint mask. If None, then all the joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). """ - raise NotImplementedError() + warnings.warn( + "Articulation.set_joint_velocity_target_mask is deprecated. Use " + "articulation.actuators.target_command.set_velocity_mask instead.", + DeprecationWarning, + stacklevel=2, + ) + self.actuators.target_command.set_velocity_mask( + value=target, + joint_mask=_as_bool_mask(joint_mask), + env_mask=_as_bool_mask(env_mask), + ) - @abstractmethod @leapp_tensor_semantics(kind=OutputKindEnum.JOINT_EFFORT, element_names_resolver=joint_names_resolver) def set_joint_effort_target_index( self, *, - target: torch.Tensor | wp.array, + target: torch.Tensor | wp.array(dtype=wp.float32), joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, + full_data: bool = False, ) -> None: """Set joint efforts into internal buffers. - .. note:: - This method expects partial data. + .. deprecated:: + Use :attr:`isaaclab.actuators.ActuatorCollection.command` and call + ``set_effort_index``. - .. tip:: - For maximum performance we recommend looking at the actual implementation of the method in the backend. - Some backends may provide optimized implementations for masks / indices. + .. note:: + This method accepts partial or full data. This function does not apply the joint targets to the simulation. It only fills the buffers with the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. Args: - target: Joint effort targets. Shape is (len(env_ids), len(joint_ids)). + target: Joint effort targets [N or N·m, depending on joint type]. Shape is + ``(len(env_ids), len(joint_ids))``, or ``(num_instances, num_joints)`` when + :paramref:`full_data` is true. joint_ids: The joint indices to set the targets for. Defaults to None (all joints). env_ids: The environment indices to set the targets for. Defaults to None (all instances). + full_data: Whether :paramref:`target` contains all articulation joints and instances. """ - raise NotImplementedError() + warnings.warn( + "Articulation.set_joint_effort_target_index is deprecated. Use " + "articulation.actuators.target_command.set_effort_index instead.", + DeprecationWarning, + stacklevel=2, + ) + self.actuators.target_command.set_effort_index( + value=target, joint_ids=joint_ids, env_ids=env_ids, full_data=full_data + ) - @abstractmethod @leapp_tensor_semantics(kind=OutputKindEnum.JOINT_EFFORT, element_names_resolver=joint_names_resolver) def set_joint_effort_target_mask( self, *, - target: torch.Tensor | wp.array, - joint_mask: wp.array | None = None, - env_mask: wp.array | None = None, + target: torch.Tensor | wp.array(dtype=wp.float32), + joint_mask: wp.array(dtype=wp.bool) | None = None, + env_mask: wp.array(dtype=wp.bool) | None = None, ) -> None: """Set joint efforts into internal buffers. + .. deprecated:: + Use :attr:`isaaclab.actuators.ActuatorCollection.command` and call + ``set_effort_mask``. + .. note:: This method expects full data. - .. tip:: - For maximum performance we recommend looking at the actual implementation of the method in the backend. - Some backends may provide optimized implementations for masks / indices. - This function does not apply the joint targets to the simulation. It only fills the buffers with the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. Args: - target: Joint effort targets. Shape is (num_instances, num_joints). + target: Joint effort targets [N or N·m, depending on joint type]. Shape is + ``(num_instances, num_joints)``. joint_mask: Joint mask. If None, then all the joints are updated. Shape is (num_joints,). env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). """ - raise NotImplementedError() + warnings.warn( + "Articulation.set_joint_effort_target_mask is deprecated. Use " + "articulation.actuators.target_command.set_effort_mask instead.", + DeprecationWarning, + stacklevel=2, + ) + self.actuators.target_command.set_effort_mask( + value=target, + joint_mask=_as_bool_mask(joint_mask), + env_mask=_as_bool_mask(env_mask), + ) """ Operations - Tendons. @@ -2615,15 +2743,6 @@ def _process_tendons(self) -> None: """Process fixed and spatial tendons.""" raise NotImplementedError() - @abstractmethod - def _apply_actuator_model(self) -> None: - """Processes joint commands for the articulation by forwarding them to the actuators. - - The actions are first processed using actuator models. Depending on the robot configuration, - the actuator models compute the joint level simulation commands and sets them into the PhysX buffers. - """ - raise NotImplementedError() - """ Internal helpers -- Debugging. """ @@ -3051,48 +3170,60 @@ def set_external_force_and_torque( def set_joint_position_target( self, - target: torch.Tensor | wp.array, - joint_ids: Sequence[int] | slice | None = None, + target: torch.Tensor | wp.array(dtype=wp.float32), + joint_ids: Sequence[int] | slice | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: - """Deprecated, same as :meth:`set_joint_position_target_index`.""" + """Deprecated. Use :attr:`isaaclab.actuators.ActuatorCollection.command` and call + ``set_position_index``. + """ warnings.warn( - "The function 'set_joint_position_target' will be deprecated in a future release. Please" - " use 'set_joint_position_target_index' instead.", + "Articulation.set_joint_position_target is deprecated. Use " + "articulation.actuators.target_command.set_position_index instead.", DeprecationWarning, stacklevel=2, ) - self.set_joint_position_target_index(target=target, joint_ids=joint_ids, env_ids=env_ids) + if isinstance(joint_ids, slice): + joint_ids = range(self.num_joints)[joint_ids] + self.actuators.target_command.set_position_index(value=target, joint_ids=joint_ids, env_ids=env_ids) def set_joint_velocity_target( self, - target: torch.Tensor | wp.array, - joint_ids: Sequence[int] | slice | None = None, + target: torch.Tensor | wp.array(dtype=wp.float32), + joint_ids: Sequence[int] | slice | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: - """Deprecated, same as :meth:`set_joint_velocity_target_index`.""" + """Deprecated. Use :attr:`isaaclab.actuators.ActuatorCollection.command` and call + ``set_velocity_index``. + """ warnings.warn( - "The function 'set_joint_velocity_target' will be deprecated in a future release. Please" - " use 'set_joint_velocity_target_index' instead.", + "Articulation.set_joint_velocity_target is deprecated. Use " + "articulation.actuators.target_command.set_velocity_index instead.", DeprecationWarning, stacklevel=2, ) - self.set_joint_velocity_target_index(target=target, joint_ids=joint_ids, env_ids=env_ids) + if isinstance(joint_ids, slice): + joint_ids = range(self.num_joints)[joint_ids] + self.actuators.target_command.set_velocity_index(value=target, joint_ids=joint_ids, env_ids=env_ids) def set_joint_effort_target( self, - target: torch.Tensor | wp.array, - joint_ids: Sequence[int] | slice | None = None, + target: torch.Tensor | wp.array(dtype=wp.float32), + joint_ids: Sequence[int] | slice | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ) -> None: - """Deprecated, same as :meth:`set_joint_effort_target_index`.""" + """Deprecated. Use :attr:`isaaclab.actuators.ActuatorCollection.command` and call + ``set_effort_index``. + """ warnings.warn( - "The function 'set_joint_effort_target' will be deprecated in a future release. Please" - " use 'set_joint_effort_target_index' instead.", + "Articulation.set_joint_effort_target is deprecated. Use " + "articulation.actuators.target_command.set_effort_index instead.", DeprecationWarning, stacklevel=2, ) - self.set_joint_effort_target_index(target=target, joint_ids=joint_ids, env_ids=env_ids) + if isinstance(joint_ids, slice): + joint_ids = range(self.num_joints)[joint_ids] + self.actuators.target_command.set_effort_index(value=target, joint_ids=joint_ids, env_ids=env_ids) def set_fixed_tendon_stiffness( self, diff --git a/source/isaaclab/isaaclab/assets/articulation/base_articulation_data.py b/source/isaaclab/isaaclab/assets/articulation/base_articulation_data.py index 25742c878f0d..141a75433aab 100644 --- a/source/isaaclab/isaaclab/assets/articulation/base_articulation_data.py +++ b/source/isaaclab/isaaclab/assets/articulation/base_articulation_data.py @@ -29,6 +29,7 @@ from . import ordering_kernels if TYPE_CHECKING: + from isaaclab.actuators import ActuatorCollection from isaaclab.utils.buffers import TimestampedBufferWarp from .ordering import ArticulationNameMap @@ -64,6 +65,68 @@ def __init__(self, root_view, device: str): """ # Set the parameters self.device = device + self._actuator_collection: ActuatorCollection | None = None + self._joint_pos_target = None + self._joint_vel_target = None + self._joint_effort_target = None + self._joint_pos_target_ta: ProxyArray | None = None + self._joint_vel_target_ta: ProxyArray | None = None + self._joint_effort_target_ta: ProxyArray | None = None + + def bind_actuator_collection(self, actuators: ActuatorCollection) -> None: + """Bind collection-owned command and telemetry aliases plus actuator compatibility projections.""" + self._actuator_collection = actuators + self._joint_pos_target = actuators.target_command.position.warp + self._joint_vel_target = actuators.target_command.velocity.warp + self._joint_effort_target = actuators.target_command.effort.warp + self._computed_torque = actuators.computed_effort.warp + self._applied_torque = actuators.applied_effort.warp + self._soft_joint_vel_limits = actuators._soft_joint_vel_limits + self._joint_pos_target_ta = actuators.target_command.position + self._joint_vel_target_ta = actuators.target_command.velocity + self._joint_effort_target_ta = actuators.target_command.effort + self._computed_torque_ta = actuators.computed_effort + self._applied_torque_ta = actuators.applied_effort + self._soft_joint_vel_limits_ta = ProxyArray(self._soft_joint_vel_limits) + + def _get_actuator_collection_proxy(self, name: str, buffer_name: str, proxy_name: str) -> ProxyArray: + collection = self._actuator_collection + if collection is not None: + command_field = { + "joint_pos_target": "position", + "joint_vel_target": "velocity", + "joint_effort_target": "effort", + }.get(name) + collection_field = { + "computed_torque": "computed_effort", + "applied_torque": "applied_effort", + }.get(name, name) + replacement = f"command.{command_field}" if command_field is not None else collection_field + warnings.warn( + f"ArticulationData.{name} is deprecated. Use articulation.actuators.{replacement} instead.", + DeprecationWarning, + stacklevel=2, + ) + return ( + getattr(collection.target_command, command_field) + if command_field is not None + else getattr(collection, collection_field) + ) + + warnings.warn( + f"ArticulationData.{name} is deprecated.", + DeprecationWarning, + stacklevel=2, + ) + buffer = getattr(self, buffer_name) + if buffer is None: + buffer = wp.zeros((self._num_instances, self._num_joints), dtype=wp.float32, device=self.device) + setattr(self, buffer_name, buffer) + proxy = getattr(self, proxy_name) + if proxy is None: + proxy = ProxyArray(buffer) + setattr(self, proxy_name, proxy) + return proxy @abstractmethod def update(self, dt: float) -> None: @@ -298,77 +361,70 @@ def default_joint_vel(self) -> ProxyArray: ## @property - @abstractmethod @leapp_tensor_semantics(kind=InputKindEnum.COMMAND_JOINT_POSITION) def joint_pos_target(self) -> ProxyArray: - """Joint position targets commanded by the user. + """Joint position targets commanded by the user [m or rad, depending on joint type]. - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). + .. deprecated:: 3.0.0 + Use ``articulation.actuators.target_command.position`` instead. - For an implicit actuator model, the targets are directly set into the simulation. - For an explicit actuator model, the targets are used to compute the joint torques (see :attr:`applied_torque`), - which are then set into the simulation. + Shape is (num_instances, num_joints), dtype = wp.float32. """ - raise NotImplementedError + return self._get_actuator_collection_proxy("joint_pos_target", "_joint_pos_target", "_joint_pos_target_ta") @property - @abstractmethod @leapp_tensor_semantics(kind=InputKindEnum.COMMAND_JOINT_VELOCITY) def joint_vel_target(self) -> ProxyArray: - """Joint velocity targets commanded by the user. + """Joint velocity targets commanded by the user [m/s or rad/s, depending on joint type]. - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). + .. deprecated:: 3.0.0 + Use ``articulation.actuators.target_command.velocity`` instead. - For an implicit actuator model, the targets are directly set into the simulation. - For an explicit actuator model, the targets are used to compute the joint torques (see :attr:`applied_torque`), - which are then set into the simulation. + Shape is (num_instances, num_joints), dtype = wp.float32. """ - raise NotImplementedError + return self._get_actuator_collection_proxy("joint_vel_target", "_joint_vel_target", "_joint_vel_target_ta") @property - @abstractmethod @leapp_tensor_semantics(kind=InputKindEnum.COMMAND_JOINT_TORQUES) def joint_effort_target(self) -> ProxyArray: - """Joint effort targets commanded by the user. + """Joint effort targets commanded by the user [N or N·m, depending on joint type]. - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). + .. deprecated:: 3.0.0 + Use ``articulation.actuators.target_command.effort`` instead. - For an implicit actuator model, the targets are directly set into the simulation. - For an explicit actuator model, the targets are used to compute the joint torques (see :attr:`applied_torque`), - which are then set into the simulation. + Shape is (num_instances, num_joints), dtype = wp.float32. """ - raise NotImplementedError + return self._get_actuator_collection_proxy( + "joint_effort_target", "_joint_effort_target", "_joint_effort_target_ta" + ) ## # Joint commands -- Explicit actuators. ## @property - @abstractmethod @leapp_tensor_semantics(kind="state/joint/computed_torque") def computed_torque(self) -> ProxyArray: - """Joint torques computed from the actuator model (before clipping). + """Computed actuator torques before clipping [N or N·m, depending on joint type]. - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). + .. deprecated:: 3.0.0 + Use ``articulation.actuators.computed_effort`` instead. - This quantity is the raw torque output from the actuator mode, before any clipping is applied. - It is exposed for users who want to inspect the computations inside the actuator model. - For instance, to penalize the learning agent for a difference between the computed and applied torques. + Shape is (num_instances, num_joints), dtype = wp.float32. """ - raise NotImplementedError + return self._get_actuator_collection_proxy("computed_torque", "_computed_torque", "_computed_torque_ta") @property - @abstractmethod @leapp_tensor_semantics(kind="state/joint/applied_torque") def applied_torque(self) -> ProxyArray: - """Joint torques applied from the actuator model (after clipping). + """Actuator torques applied after clipping [N or N·m, depending on joint type]. - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). + .. deprecated:: 3.0.0 + Use ``articulation.actuators.applied_effort`` instead. - These torques are set into the simulation, after clipping the :attr:`computed_torque` based on the - actuator model. + Shape is (num_instances, num_joints), dtype = wp.float32. """ - raise NotImplementedError + return self._get_actuator_collection_proxy("applied_torque", "_applied_torque", "_applied_torque_ta") ## # Joint properties. @@ -378,11 +434,10 @@ def applied_torque(self) -> ProxyArray: @abstractmethod @leapp_tensor_semantics(const=True) def joint_stiffness(self) -> ProxyArray: - """Joint stiffness provided to the simulation. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). + """Solver joint-drive stiffness [N/m or N·m/rad, depending on joint type]. - In the case of explicit actuators, the value for the corresponding joints is zero. + Shape is (num_instances, num_joints), dtype = wp.float32. Explicit + actuator joints report zero because their model owns the gains. """ raise NotImplementedError @@ -390,11 +445,10 @@ def joint_stiffness(self) -> ProxyArray: @abstractmethod @leapp_tensor_semantics(const=True) def joint_damping(self) -> ProxyArray: - """Joint damping provided to the simulation. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). + """Solver joint-drive damping [N·s/m or N·m·s/rad, depending on joint type]. - In the case of explicit actuators, the value for the corresponding joints is zero. + Shape is (num_instances, num_joints), dtype = wp.float32. Explicit + actuator joints report zero because their model owns the gains. """ raise NotImplementedError @@ -487,27 +541,17 @@ def soft_joint_pos_limits(self) -> ProxyArray: raise NotImplementedError @property - @abstractmethod @leapp_tensor_semantics(const=True) def soft_joint_vel_limits(self) -> ProxyArray: - """Soft joint velocity limits for all joints. + """Actuator-resolved soft joint velocity limits [m/s or rad/s, depending on joint type]. - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). + Shape is (num_instances, num_joints), dtype = wp.float32. - These are obtained from the actuator model. It may differ from :attr:`joint_vel_limits` if the actuator model - has a variable velocity limit model. For instance, in a variable gear ratio actuator model. + These values are produced by the actuator model and can differ from :attr:`joint_vel_limits` for a + state-dependent velocity-limit model, such as one with a variable gear ratio. They are compatibility outputs; + the solver velocity limits remain :attr:`joint_vel_limits`. """ - raise NotImplementedError - - @property - @abstractmethod - @leapp_tensor_semantics(const=True) - def gear_ratio(self) -> ProxyArray: - """Gear ratio for relating motor torques to applied Joint torques. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - """ - raise NotImplementedError + return self._soft_joint_vel_limits_ta ## # Fixed tendon properties. diff --git a/source/isaaclab/isaaclab/assets/articulation/ordering_kernels.py b/source/isaaclab/isaaclab/assets/articulation/ordering_kernels.py index bca8ae76d846..1d0af90bb27e 100644 --- a/source/isaaclab/isaaclab/assets/articulation/ordering_kernels.py +++ b/source/isaaclab/isaaclab/assets/articulation/ordering_kernels.py @@ -171,6 +171,41 @@ def reorder_joint_targets_user_to_backend( backend_vel_target[env_id, backend_id] = user_vel_target[env_id, user_id] +def launch_reorder_joint_targets_user_to_backend( + *, + user_effort: wp.array2d(dtype=wp.float32), + user_pos_target: wp.array2d(dtype=wp.float32), + user_vel_target: wp.array2d(dtype=wp.float32), + backend_to_user: wp.array(dtype=wp.int32), + write_effort: bool, + write_pos_target: bool, + write_vel_target: bool, + write_joint_act: bool, + backend_effort: wp.array2d(dtype=wp.float32), + backend_pos_target: wp.array2d(dtype=wp.float32), + backend_vel_target: wp.array2d(dtype=wp.float32), + backend_joint_act: wp.array2d(dtype=wp.float32) | None, + device: str, +) -> None: + """Launch the fused public-to-backend joint-target reorder.""" + wp.launch( + reorder_joint_targets_user_to_backend, + dim=user_effort.shape, + inputs=[ + user_effort, + user_pos_target, + user_vel_target, + backend_to_user, + write_effort, + write_pos_target, + write_vel_target, + write_joint_act, + ], + outputs=[backend_effort, backend_pos_target, backend_vel_target, backend_joint_act], + device=device, + ) + + @wp.kernel def reorder_joint_state_backend_to_user( backend_pos: wp.array2d(dtype=wp.float32), diff --git a/source/isaaclab/isaaclab/benchmark/asset_suites/suites.py b/source/isaaclab/isaaclab/benchmark/asset_suites/suites.py index 6960d5d74475..52378fdac50e 100644 --- a/source/isaaclab/isaaclab/benchmark/asset_suites/suites.py +++ b/source/isaaclab/isaaclab/benchmark/asset_suites/suites.py @@ -571,7 +571,6 @@ def _rigid_masks(prefix: str) -> tuple[AssetMethodSpec, ...]: "body_link_jacobian_w", "default_joint_pos", "default_joint_vel", - "gear_ratio", "joint_acc", "joint_armature", "joint_damping", diff --git a/source/isaaclab/isaaclab/benchmark/entrypoints/startup.py b/source/isaaclab/isaaclab/benchmark/entrypoints/startup.py index 29b8730e27cf..a0c30db1fae8 100644 --- a/source/isaaclab/isaaclab/benchmark/entrypoints/startup.py +++ b/source/isaaclab/isaaclab/benchmark/entrypoints/startup.py @@ -286,7 +286,8 @@ def run(argv: list[str]) -> BenchmarkResult | None: first_step_time_begin = time.perf_counter_ns() first_step_profile.enable() try: - env.step(actions) + with torch.inference_mode(): + env.step(actions) finally: first_step_profile.disable() diff --git a/source/isaaclab/isaaclab/benchmark/stepping.py b/source/isaaclab/isaaclab/benchmark/stepping.py index d771f9696237..12ffeec0b71f 100644 --- a/source/isaaclab/isaaclab/benchmark/stepping.py +++ b/source/isaaclab/isaaclab/benchmark/stepping.py @@ -248,7 +248,9 @@ def run_runtime_loop(env, num_steps: int, *, reset: bool = True) -> list[float]: Optionally calls ``env.reset()`` once before the loop, then on each frame samples random actions via :func:`sample_random_actions`, steps the - environment, and records the elapsed wall-clock time for that step. + environment, and records the elapsed wall-clock time for that step. Reset, + action sampling, and environment stepping run under + ``torch.inference_mode()``. Args: env: A Gym-compatible environment. @@ -258,17 +260,20 @@ def run_runtime_loop(env, num_steps: int, *, reset: bool = True) -> list[float]: Returns: A list of length ``num_steps`` containing per-step wall times [s]. """ - if reset: - env.reset() + import torch # noqa: PLC0415 step_times: list[float] = [] - for _ in range(num_steps): - actions = sample_random_actions(env) - t0 = time.perf_counter_ns() - env.step(actions) - t1 = time.perf_counter_ns() - step_times.append((t1 - t0) / 1e9) + with torch.inference_mode(): + if reset: + env.reset() + + for _ in range(num_steps): + actions = sample_random_actions(env) + t0 = time.perf_counter_ns() + env.step(actions) + t1 = time.perf_counter_ns() + step_times.append((t1 - t0) / 1e9) return step_times @@ -318,12 +323,12 @@ def _extract_success(extras) -> float | None: def run_play_loop(env, policy, num_steps: int) -> tuple[list[float], MeanStd | None, MeanStd | None, float | None]: """Roll out *policy* in *env* for *num_steps* steps and aggregate episode metrics. - Resets the environment, then on each frame runs the policy under - ``torch.inference_mode()`` and steps the environment, recording the - per-step wall time [s]. Per-environment returns and lengths are accumulated - and, whenever an environment signals ``done``, that episode's return, - length, and (if present) success value are recorded and the environment's - accumulators are reset. + Resets the environment, then on each frame runs the policy and steps the + environment under ``torch.inference_mode()``, recording the per-step wall + time [s]. Per-environment returns and lengths are accumulated and, whenever + an environment signals ``done``, that episode's return, length, and (if + present) success value are recorded and the environment's accumulators are + reset. Both the four-tuple ``(obs, reward, dones, extras)`` and the Gym five-tuple ``(obs, reward, terminated, truncated, info)`` step signatures are accepted; @@ -369,7 +374,7 @@ def run_play_loop(env, policy, num_steps: int) -> tuple[list[float], MeanStd | N t0 = time.perf_counter_ns() with torch.inference_mode(): actions = policy(obs) - result = env.step(actions) + result = env.step(actions) t1 = time.perf_counter_ns() step_times.append((t1 - t0) / 1e9) diff --git a/source/isaaclab/isaaclab/envs/mdp/events.py b/source/isaaclab/isaaclab/envs/mdp/events.py index 3c6b5de833aa..e6706dc3f2ad 100644 --- a/source/isaaclab/isaaclab/envs/mdp/events.py +++ b/source/isaaclab/isaaclab/envs/mdp/events.py @@ -24,7 +24,6 @@ import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils -from isaaclab.actuators import ImplicitActuator from isaaclab.managers import EventTermCfg, ManagerTermBase, SceneEntityCfg from isaaclab.utils.version import compare_versions @@ -1388,22 +1387,43 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): self.default_joint_stiffness = self.asset.data.joint_stiffness.torch.clone() self.default_joint_damping = self.asset.data.joint_damping.torch.clone() - # For explicit Lab actuators the sim-level stiffness/damping is zeroed out, - # so patch the defaults with the actual actuator PD gains. - for actuator in self.asset.actuators.values(): - if not isinstance(actuator, ImplicitActuator): - joint_ids = actuator.joint_indices - self.default_joint_stiffness[:, joint_ids] = actuator.stiffness - self.default_joint_damping[:, joint_ids] = actuator.damping - # Same for explicit Newton actuators on either backend — their kp/kd - # live on the per-actuator controller arrays (not on a Lab Actuator - # object), so the asset exposes a per-articulation snapshot taken - # at articulation init time. - newton_default_stiffness = getattr(self.asset, "newton_default_stiffness", None) - if newton_default_stiffness is not None: - joint_ids = self.asset.newton_managed_local_joints - self.default_joint_stiffness[:, joint_ids] = newton_default_stiffness[:, joint_ids] - self.default_joint_damping[:, joint_ids] = self.asset.newton_default_damping[:, joint_ids] + # Ownership decides the gain source and write path per group: implicit groups are + # articulation-owned, Newton-executed groups are controller-owned (their mapping + # entries are the Newton actuator objects), and Lab explicit groups own their tensors. + from isaaclab.actuators import IdealPDActuator # noqa: PLC0415 + + collection = self.asset.actuators + self._native_group_names = getattr(collection, "_native_group_names", set()) + self._gain_actuators = { + name: actuator + for name, actuator in collection.items() + if name in self._native_group_names + or getattr(actuator, "is_implicit_model", False) + or isinstance(actuator, IdealPDActuator) + } + group_joint_indices = getattr(collection, "_group_joint_indices", None) + self._group_joint_indices = { + name: (group_joint_indices[name] if group_joint_indices is not None else actuator.joint_indices) + for name, actuator in self._gain_actuators.items() + } + self.default_actuator_stiffness: dict[str, torch.Tensor] = {} + self.default_actuator_damping: dict[str, torch.Tensor] = {} + from isaaclab.actuators.newton import read_group_parameter # noqa: PLC0415 + + for name, actuator in self._gain_actuators.items(): + joint_ids = self._group_joint_indices[name] + if name in self._native_group_names: + stiffness = read_group_parameter(collection, name, "controller", "kp") + damping = read_group_parameter(collection, name, "controller", "kd") + else: + stiffness = actuator.stiffness + damping = actuator.damping + if not getattr(actuator, "is_implicit_model", False): + # Explicit and Newton PD gains replace the zeroed solver gains in the defaults. + self.default_joint_stiffness[:, joint_ids] = stiffness + self.default_joint_damping[:, joint_ids] = damping + self.default_actuator_stiffness[name] = stiffness.clone() + self.default_actuator_damping[name] = damping.clone() # check for valid operation if cfg.params["operation"] == "scale": @@ -1429,6 +1449,8 @@ def __call__( operation: Literal["add", "scale", "abs"] = "abs", distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform", ): + from isaaclab.actuators.newton import write_group_parameter # noqa: PLC0415 + # Resolve environment ids if env_ids is None: env_ids = torch.arange(env.scene.num_envs, device=self.asset.device) @@ -1439,22 +1461,23 @@ def randomize(data: torch.Tensor, params: tuple[float, float]) -> torch.Tensor: ) # Loop through actuators and randomize gains - for actuator in self.asset.actuators.values(): + for actuator_name, actuator in self._gain_actuators.items(): + group_joint_indices = self._group_joint_indices[actuator_name] if isinstance(self.asset_cfg.joint_ids, slice): # we take all the joints of the actuator actuator_indices = slice(None) - if isinstance(actuator.joint_indices, slice): + if isinstance(group_joint_indices, slice): global_indices = slice(None) - elif isinstance(actuator.joint_indices, torch.Tensor): - global_indices = actuator.joint_indices.to(self.asset.device) + elif isinstance(group_joint_indices, torch.Tensor): + global_indices = group_joint_indices.to(self.asset.device) else: raise TypeError("Actuator joint indices must be a slice or a torch.Tensor.") - elif isinstance(actuator.joint_indices, slice): + elif isinstance(group_joint_indices, slice): # we take the joints defined in the asset config global_indices = actuator_indices = torch.tensor(self.asset_cfg.joint_ids, device=self.asset.device) else: # we take the intersection of the actuator joints and the asset config joints - actuator_joint_indices = actuator.joint_indices + actuator_joint_indices = group_joint_indices asset_joint_ids = torch.tensor(self.asset_cfg.joint_ids, device=self.asset.device) # the indices of the joints in the actuator that have to be randomized actuator_indices = torch.nonzero(torch.isin(actuator_joint_indices, asset_joint_ids)).view(-1) @@ -1462,70 +1485,66 @@ def randomize(data: torch.Tensor, params: tuple[float, float]) -> torch.Tensor: continue # maps actuator indices that have to be randomized to global joint indices global_indices = actuator_joint_indices[actuator_indices] + if isinstance(global_indices, slice): + writer_joint_ids = torch.arange(self.asset.num_joints, device=self.asset.device, dtype=torch.long) + else: + writer_joint_ids = global_indices.to(device=self.asset.device, dtype=torch.long) + is_native = actuator_name in self._native_group_names + # Native group writes are group-targeted: they take positions within the group's joints. + group_columns = None if isinstance(actuator_indices, slice) else actuator_indices # Randomize stiffness if stiffness_distribution_params is not None: - stiffness = actuator.stiffness[env_ids].clone() - stiffness[:, actuator_indices] = self.default_joint_stiffness[env_ids][:, global_indices].clone() + if is_native: + # Native gains are controller-owned; randomization always starts from the defaults. + stiffness = self.default_actuator_stiffness[actuator_name][env_ids].clone() + else: + stiffness = actuator.stiffness[env_ids].clone() + stiffness[:, actuator_indices] = self.default_actuator_stiffness[actuator_name][env_ids][ + :, actuator_indices + ] randomize(stiffness, stiffness_distribution_params) - actuator.stiffness[env_ids] = stiffness - if isinstance(actuator, ImplicitActuator): + if getattr(actuator, "is_implicit_model", False): self.asset.write_joint_stiffness_to_sim_index( - stiffness=stiffness, joint_ids=actuator.joint_indices, env_ids=env_ids + stiffness=stiffness[:, actuator_indices], joint_ids=writer_joint_ids, env_ids=env_ids ) + elif is_native: + write_group_parameter( + self.asset.actuators, + actuator_name, + "controller", + "kp", + values=stiffness[:, actuator_indices], + env_ids=env_ids, + joint_ids=group_columns, + ) + else: + actuator.stiffness[env_ids] = stiffness # Randomize damping if damping_distribution_params is not None: - damping = actuator.damping[env_ids].clone() - damping[:, actuator_indices] = self.default_joint_damping[env_ids][:, global_indices].clone() + if is_native: + damping = self.default_actuator_damping[actuator_name][env_ids].clone() + else: + damping = actuator.damping[env_ids].clone() + damping[:, actuator_indices] = self.default_actuator_damping[actuator_name][env_ids][ + :, actuator_indices + ] randomize(damping, damping_distribution_params) - actuator.damping[env_ids] = damping - if isinstance(actuator, ImplicitActuator): + if getattr(actuator, "is_implicit_model", False): self.asset.write_joint_damping_to_sim_index( - damping=damping, joint_ids=actuator.joint_indices, env_ids=env_ids + damping=damping[:, actuator_indices], joint_ids=writer_joint_ids, env_ids=env_ids ) - - # Push DR updates to explicit Newton-actuator controllers via the asset's - # own write methods. Each backend's articulation iterates the adapter's - # actuators and propagates per actuator, using the appropriate backend - # mechanism (Newton ``ArticulationView`` on the Newton backend, an - # in-place scatter kernel on PhysX). - if not hasattr(self.asset, "write_actuator_stiffness_to_sim"): - return - - if isinstance(self.asset_cfg.joint_ids, slice): - joint_ids = torch.arange(self.asset.num_joints, device=self.asset.device, dtype=torch.long) - else: - joint_ids = torch.tensor(self.asset_cfg.joint_ids, device=self.asset.device, dtype=torch.long) - - if stiffness_distribution_params is not None: - new_stiffness = self.default_joint_stiffness[env_ids][:, joint_ids].clone() - _randomize_prop_by_op( - new_stiffness, - stiffness_distribution_params, - dim_0_ids=None, - dim_1_ids=slice(None), - operation=operation, - distribution=distribution, - ) - self.asset.write_actuator_stiffness_to_sim( - stiffness=new_stiffness, - env_ids=env_ids, - joint_ids=joint_ids, - ) - if damping_distribution_params is not None: - new_damping = self.default_joint_damping[env_ids][:, joint_ids].clone() - _randomize_prop_by_op( - new_damping, - damping_distribution_params, - dim_0_ids=None, - dim_1_ids=slice(None), - operation=operation, - distribution=distribution, - ) - self.asset.write_actuator_damping_to_sim( - damping=new_damping, - env_ids=env_ids, - joint_ids=joint_ids, - ) + elif is_native: + write_group_parameter( + self.asset.actuators, + actuator_name, + "controller", + "kd", + values=damping[:, actuator_indices], + env_ids=env_ids, + joint_ids=group_columns, + ) + else: + actuator.damping[env_ids] = damping class randomize_joint_parameters(ManagerTermBase): diff --git a/source/isaaclab/isaaclab/envs/mdp/observations.py b/source/isaaclab/isaaclab/envs/mdp/observations.py index 74f464e6b6e4..4bdc05664686 100644 --- a/source/isaaclab/isaaclab/envs/mdp/observations.py +++ b/source/isaaclab/isaaclab/envs/mdp/observations.py @@ -290,7 +290,7 @@ def joint_effort(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCf """ # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] - return asset.data.applied_torque.torch[:, asset_cfg.joint_ids] + return asset.actuators.applied_effort.torch[:, asset_cfg.joint_ids] """ diff --git a/source/isaaclab/isaaclab/envs/mdp/rewards.py b/source/isaaclab/isaaclab/envs/mdp/rewards.py index 7d42f1e5a5e3..1fd1eff06e8f 100644 --- a/source/isaaclab/isaaclab/envs/mdp/rewards.py +++ b/source/isaaclab/isaaclab/envs/mdp/rewards.py @@ -144,7 +144,7 @@ def joint_torques_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEn """ # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] - return torch.sum(torch.square(asset.data.applied_torque.torch[:, asset_cfg.joint_ids]), dim=1) + return torch.sum(torch.square(asset.actuators.applied_effort.torch[:, asset_cfg.joint_ids]), dim=1) def joint_vel_l1(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: @@ -249,8 +249,8 @@ def applied_torque_limits(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = Sc # compute out of limits constraints # TODO: We need to fix this to support implicit joints. out_of_limits = torch.abs( - asset.data.applied_torque.torch[:, asset_cfg.joint_ids] - - asset.data.computed_torque.torch[:, asset_cfg.joint_ids] + asset.actuators.applied_effort.torch[:, asset_cfg.joint_ids] + - asset.actuators.computed_effort.torch[:, asset_cfg.joint_ids] ) return torch.sum(out_of_limits, dim=1) diff --git a/source/isaaclab/isaaclab/envs/mdp/terminations.py b/source/isaaclab/isaaclab/envs/mdp/terminations.py index 0879994d4b94..9f5e0a37702f 100644 --- a/source/isaaclab/isaaclab/envs/mdp/terminations.py +++ b/source/isaaclab/isaaclab/envs/mdp/terminations.py @@ -169,8 +169,8 @@ def joint_effort_out_of_limit( asset: Articulation = env.scene[asset_cfg.name] # check if any joint effort is out of limit out_of_limits = ~torch.isclose( - asset.data.computed_torque.torch[:, asset_cfg.joint_ids], - asset.data.applied_torque.torch[:, asset_cfg.joint_ids], + asset.actuators.computed_effort.torch[:, asset_cfg.joint_ids], + asset.actuators.applied_effort.torch[:, asset_cfg.joint_ids], ) return torch.any(out_of_limits, dim=1) diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas_actuators.py b/source/isaaclab/isaaclab/sim/schemas/schemas_actuators.py index 8f91689fa433..dd8e0056ff42 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas_actuators.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas_actuators.py @@ -8,7 +8,7 @@ :func:`define_actuator_properties` translates IsaacLab actuator configs into ``NewtonActuator`` USD prims. Both the Newton ``ModelBuilder.add_usd`` path and the PhysX adapter's -:meth:`~isaaclab_newton.actuators.adapter.NewtonActuatorAdapter.from_usd` +:meth:`~isaaclab.actuators.newton.adapter.NewtonActuatorAdapter.from_usd` read the same authored prims, ensuring both backends construct :class:`~newton.actuators.Actuator` instances with matching parameters. @@ -20,16 +20,76 @@ from __future__ import annotations -import logging import re from typing import Any -from pxr import Sdf, Usd +from pxr import Sdf, Usd, UsdPhysics -from isaaclab.actuators import ImplicitActuator -from isaaclab.utils.string import resolve_matching_names +from isaaclab.actuators._compat import _resolve_limit_aliases +from isaaclab.actuators.actuator_base_cfg import _is_implicit_actuator_cfg +from isaaclab.utils.string import _resolve_matching_values_dense, resolve_matching_names, string_to_callable -logger = logging.getLogger(__name__) + +def _resolve_actuator_class(class_type: type | str) -> type: + """Resolve and validate an actuator class reference for authoring identity checks.""" + from isaaclab.actuators import ActuatorBase # noqa: PLC0415 + + if isinstance(class_type, str): + try: + class_type = string_to_callable(str(class_type)) + except (AttributeError, ImportError, ValueError) as error: + raise ValueError(f"Unable to resolve actuator class '{class_type}'.") from error + if not isinstance(class_type, type) or not issubclass(class_type, ActuatorBase): + raise ValueError(f"Actuator class must derive from ActuatorBase, got {class_type!r}.") + return class_type + + +def _is_newton_native_actuator_cfg(cfg: Any) -> bool: + """Return whether an actuator config can be authored as a Newton actuator.""" + from isaaclab.actuators import DCMotorCfg, DelayedPDActuatorCfg # noqa: PLC0415 + from isaaclab.actuators.actuator_net import ActuatorNetLSTM, ActuatorNetMLP # noqa: PLC0415 + from isaaclab.actuators.actuator_net_cfg import ActuatorNetLSTMCfg, ActuatorNetMLPCfg # noqa: PLC0415 + from isaaclab.actuators.actuator_pd import ( # noqa: PLC0415 + DCMotor, + DelayedPDActuator, + IdealPDActuator, + RemotizedPDActuator, + ) + from isaaclab.actuators.actuator_pd_cfg import IdealPDActuatorCfg, RemotizedPDActuatorCfg # noqa: PLC0415 + + supported_cfg_types = ( + (ActuatorNetMLPCfg, ActuatorNetMLP), + (ActuatorNetLSTMCfg, ActuatorNetLSTM), + (RemotizedPDActuatorCfg, RemotizedPDActuator), + (DelayedPDActuatorCfg, DelayedPDActuator), + (DCMotorCfg, DCMotor), + (IdealPDActuatorCfg, IdealPDActuator), + ) + try: + resolved_class = _resolve_actuator_class(cfg.class_type) + except ValueError: + return False + for cfg_type, actuator_type in supported_cfg_types: + if isinstance(cfg, cfg_type): + return resolved_class is actuator_type + return False + + +def _validate_newton_native_actuator_cfgs(actuator_cfgs: dict[str, Any]) -> None: + """Reject explicit actuator configurations that Newton cannot author.""" + unsupported_groups = [] + for group_name, cfg in actuator_cfgs.items(): + try: + is_implicit = _is_implicit_actuator_cfg(cfg) + except ValueError: + is_implicit = False + if not is_implicit and not _is_newton_native_actuator_cfg(cfg): + unsupported_groups.append(f"'{group_name}' ({type(cfg).__name__})") + if unsupported_groups: + raise ValueError( + "Newton-native actuator execution does not support " + f"{', '.join(unsupported_groups)}. Disable 'use_newton_actuators' or use a supported actuator config." + ) def resolve_per_dof( @@ -111,6 +171,9 @@ def define_actuator_properties( :class:`~isaaclab.actuators.ActuatorBaseCfg`. stage: USD stage to author on. When ``None``, the current stage is used. + + Raises: + ValueError: If Newton-native execution is enabled and an explicit actuator config is unsupported. """ from isaaclab.sim import SimulationContext # noqa: PLC0415 @@ -143,6 +206,8 @@ def _author_actuator_prims( if not art_prim.IsValid(): raise ValueError(f"Articulation prim not found: {articulation_prim_path}") + _validate_newton_native_actuator_cfgs(actuator_cfgs) + joint_inventory = _collect_joint_prims(art_prim) all_joint_names = list(joint_inventory.keys()) @@ -150,18 +215,18 @@ def _author_actuator_prims( cfg_entries: list[tuple[str, Any, list[str]]] = [] for group_name, cfg in actuator_cfgs.items(): - cls_type = cfg.class_type - is_implicit = ( - "ImplicitActuator" in cls_type if isinstance(cls_type, str) else issubclass(cls_type, ImplicitActuator) - ) - if is_implicit: + if _is_implicit_actuator_cfg(cfg): continue _ids, joint_names = resolve_matching_names(cfg.joint_names_expr, all_joint_names) if not joint_names: continue - cfg_entries.append((group_name, cfg, joint_names)) + resolved_cfg = cfg.copy() + # Collection construction emits the deprecation warning later in the + # normal asset lifecycle. Authoring only needs the normalized value. + _resolve_limit_aliases(group_name, resolved_cfg, joint_names, warn_deprecated=False) + cfg_entries.append((group_name, resolved_cfg, joint_names)) for jname in joint_names: covered_joint_paths.add(joint_inventory[jname]) @@ -169,36 +234,33 @@ def _author_actuator_prims( from isaaclab.actuators import DCMotorCfg, DelayedPDActuatorCfg # noqa: PLC0415 from isaaclab.actuators.actuator_net_cfg import ActuatorNetLSTMCfg, ActuatorNetMLPCfg # noqa: PLC0415 - from isaaclab.actuators.actuator_pd_cfg import IdealPDActuatorCfg, RemotizedPDActuatorCfg # noqa: PLC0415 - - _SUPPORTED_CFG_TYPES = ( - IdealPDActuatorCfg, - DCMotorCfg, - DelayedPDActuatorCfg, - RemotizedPDActuatorCfg, - ActuatorNetMLPCfg, - ActuatorNetLSTMCfg, - ) + from isaaclab.actuators.actuator_pd_cfg import RemotizedPDActuatorCfg # noqa: PLC0415 for group_name, cfg, joint_names in cfg_entries: - if not isinstance(cfg, _SUPPORTED_CFG_TYPES): - logger.warning( - "Actuator group '%s' uses config type '%s' which is not supported by Newton-native" - " actuator authoring. The group will be skipped.", - group_name, - type(cfg).__name__, - ) - continue stiffness_map = resolve_per_dof(getattr(cfg, "stiffness", None), joint_names) damping_map = resolve_per_dof(getattr(cfg, "damping", None), joint_names) - effort_map = resolve_per_dof(getattr(cfg, "effort_limit", None), joint_names) is_neural = isinstance(cfg, (ActuatorNetMLPCfg, ActuatorNetLSTMCfg)) is_remotized = isinstance(cfg, RemotizedPDActuatorCfg) is_dc_motor = isinstance(cfg, DCMotorCfg) is_delayed = isinstance(cfg, DelayedPDActuatorCfg) - vel_limit_map = resolve_per_dof(getattr(cfg, "velocity_limit", None), joint_names) if is_dc_motor else {} + configured_effort_limit = getattr(cfg, "actuator_effort_limit", None) + effort_map: dict[str, float] = {} + if not is_remotized: + if configured_effort_limit is None: + for joint_name in joint_names: + authored_effort_limit = _get_authored_joint_effort_limit(stage, joint_inventory[joint_name]) + if authored_effort_limit is not None: + effort_map[joint_name] = authored_effort_limit + else: + effort_map = dict( + zip(joint_names, _resolve_matching_values_dense(configured_effort_limit, joint_names)) + ) + + vel_limit_map = ( + resolve_per_dof(getattr(cfg, "actuator_velocity_limit", None), joint_names) if is_dc_motor else {} + ) sat_effort_map = resolve_per_dof(getattr(cfg, "saturation_effort", None), joint_names) if is_dc_motor else {} raw_delay = getattr(cfg, "max_delay", 0) if is_delayed else 0 @@ -239,7 +301,7 @@ def _author_actuator_prims( attrs["velocity_limit"] = vel_limit_map[jname] if jname in effort_map: attrs["max_motor_effort"] = effort_map[jname] - elif jname in effort_map: + elif not is_remotized and jname in effort_map: schemas.append("NewtonMaxEffortClampingAPI") attrs["max_effort"] = effort_map[jname] @@ -294,6 +356,19 @@ def _snake_to_camel(name: str) -> str: return _SNAKE_TO_CAMEL_RE.sub(lambda m: m.group(1).upper(), name) +def _get_authored_joint_effort_limit(stage: Usd.Stage, joint_prim_path: str) -> float | None: + """Read a revolute or prismatic joint's authored USD drive effort limit.""" + joint_prim = stage.GetPrimAtPath(joint_prim_path) + if joint_prim.IsA(UsdPhysics.RevoluteJoint): + drive_name = "angular" + elif joint_prim.IsA(UsdPhysics.PrismaticJoint): + drive_name = "linear" + else: + return None + value = UsdPhysics.DriveAPI(joint_prim, drive_name).GetMaxForceAttr().Get() + return None if value is None else float(value) + + def _collect_joint_prims(art_prim: Any) -> dict[str, str]: """Collect all joint prims under an articulation subtree. @@ -343,10 +418,11 @@ def _resave_checkpoint_with_metadata( ) -> str: """Re-save a neural-network checkpoint with updated metadata. - Loads the original TorchScript or dict checkpoint, merges *metadata* - into any existing metadata (Lab config values take precedence), and - writes the result to a temporary ``.pt`` file that persists for the - lifetime of the process. + Resolves the configured path through the shared asset cache, loads the + original TorchScript or dict checkpoint, merges *metadata* into any + existing metadata (Lab config values take precedence), and writes the + result to a temporary ``.pt`` file that persists for the lifetime of the + process. Returns: Path to the temporary checkpoint file. @@ -356,14 +432,18 @@ def _resave_checkpoint_with_metadata( import torch # noqa: PLC0415 + from isaaclab.utils.assets import retrieve_file_path # noqa: PLC0415 + + local_path = retrieve_file_path(original_path) + extra_files: dict[str, str] = {"metadata.json": ""} is_torchscript = True try: - net = torch.jit.load(original_path, map_location="cpu", _extra_files=extra_files) + net = torch.jit.load(local_path, map_location="cpu", _extra_files=extra_files) existing_meta = json.loads(extra_files["metadata.json"]) if extra_files["metadata.json"] else {} except Exception: is_torchscript = False - checkpoint = torch.load(original_path, map_location="cpu", weights_only=False) + checkpoint = torch.load(local_path, map_location="cpu", weights_only=False) if not isinstance(checkpoint, dict) or "model" not in checkpoint: raise ValueError( f"Cannot load checkpoint at '{original_path}'; " diff --git a/source/isaaclab/isaaclab/sim/simulation_cfg.py b/source/isaaclab/isaaclab/sim/simulation_cfg.py index 971a4fbc58e7..d3105f8011a6 100644 --- a/source/isaaclab/isaaclab/sim/simulation_cfg.py +++ b/source/isaaclab/isaaclab/sim/simulation_cfg.py @@ -85,19 +85,16 @@ class SimulationCfg: """ use_newton_actuators: bool = False - """Use Newton-native actuators instead of IsaacLab explicit actuator models. - - When ``True``, explicit actuator configs (e.g. :class:`IdealPDActuatorCfg`, - :class:`DCMotorCfg`) are translated into ``NewtonActuator`` USD prims and - stepped by the physics engine. The Lab config values (stiffness, damping, - effort_limit, etc.) take precedence: for every joint covered by a Lab - actuator config, any existing ``NewtonActuator`` prim targeting that joint - is replaced by one synthesised from the config. Joints that are *not* - covered by a Lab config keep their USD-authored actuators (if any). - - :class:`ImplicitActuatorCfg` entries are still instantiated normally and - their gains are written to the simulation, so joints that use implicit - actuation continue to work as expected. + """Use native actuators for supported explicit actuator configurations. + + When ``True``, supported explicit configs, such as :class:`IdealPDActuatorCfg` + and :class:`DCMotorCfg`, author ``NewtonActuator`` USD prims. Newton executes + them in its solver. PhysX and OVPhysX execute them through a shared host + adapter during :meth:`~isaaclab.assets.Articulation.write_data_to_sim`. + + Config values take precedence over existing USD actuators for covered joints. + Joints without a config keep their USD-authored actuators. Implicit actuators + are unchanged: the solver applies their drive gains. """ physics: PhysicsCfg | None = None diff --git a/source/isaaclab/isaaclab/test/integration_scene_cfgs.py b/source/isaaclab/isaaclab/test/integration_scene_cfgs.py index 8f716149fa1e..3b31a250232c 100644 --- a/source/isaaclab/isaaclab/test/integration_scene_cfgs.py +++ b/source/isaaclab/isaaclab/test/integration_scene_cfgs.py @@ -24,13 +24,13 @@ actuators={ "cart_actuator": ImplicitActuatorCfg( joint_names_expr=["slider_to_cart"], - effort_limit_sim=400.0, + joint_effort_limit=400.0, stiffness=0.0, damping=10.0, ), "pole_actuator": ImplicitActuatorCfg( joint_names_expr=["cart_to_pole"], - effort_limit_sim=400.0, + joint_effort_limit=400.0, stiffness=0.0, damping=0.0, ), diff --git a/source/isaaclab/isaaclab/test/utils/actuator_equivalence.py b/source/isaaclab/isaaclab/test/utils/actuator_equivalence.py new file mode 100644 index 000000000000..35aa298a2f78 --- /dev/null +++ b/source/isaaclab/isaaclab/test/utils/actuator_equivalence.py @@ -0,0 +1,453 @@ +# 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 + +"""Backend-agnostic helpers for Lab-vs-Newton actuator equivalence tests. + +Shared between the Newton-backend and PhysX-backend actuator test twins: + +* the actuator configuration dictionaries under test (ANYmal-C leg groups + plus the cartpole explicit group), +* :class:`EquivalenceAssertionsMixin` with the trajectory/telemetry + ``test_*_match`` oracles, +* dummy TorchScript checkpoint factories for the neural actuator tests, +* mock scene/env plumbing for driving ``randomize_actuator_gains``, +* :class:`ActuatorStateResetBase` with the per-env actuator state reset + scenario. + +This module must stay free of backend packages (``isaaclab_newton``, +``isaaclab_physx``) and of ``isaaclab_assets``; everything backend-specific +is injected by the twin test files through subclass hooks. +""" + +import json +import tempfile + +import torch +import warp as wp + +import isaaclab.sim as sim_utils +from isaaclab.actuators import DCMotorCfg, DelayedPDActuatorCfg, IdealPDActuatorCfg, ImplicitActuatorCfg +from isaaclab.sim import SimulationCfg, build_simulation_context + +# --------------------------------------------------------------------------- +# Actuator configurations under test +# --------------------------------------------------------------------------- + +IDEAL_PD_ACTUATORS = { + "legs": IdealPDActuatorCfg( + joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], + stiffness=40.0, + damping=5.0, + actuator_effort_limit=80.0, + ), +} + +DC_MOTOR_ACTUATORS = { + "legs": DCMotorCfg( + joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], + saturation_effort=120.0, + actuator_effort_limit=80.0, + actuator_velocity_limit=7.5, + stiffness={".*": 40.0}, + damping={".*": 5.0}, + ), +} + +MIXED_ACTUATORS = { + "hips": IdealPDActuatorCfg( + joint_names_expr=[".*HAA"], + stiffness=40.0, + damping=5.0, + actuator_effort_limit=80.0, + ), + "knees": DCMotorCfg( + joint_names_expr=[".*HFE", ".*KFE"], + saturation_effort=120.0, + actuator_effort_limit=80.0, + actuator_velocity_limit=7.5, + stiffness={".*": 40.0}, + damping={".*": 5.0}, + ), +} + +MIXED_WITH_IMPLICIT_ACTUATORS = { + "hips": ImplicitActuatorCfg( + joint_names_expr=[".*HAA"], + stiffness=40.0, + damping=5.0, + ), + "thighs": IdealPDActuatorCfg( + joint_names_expr=[".*HFE"], + stiffness=40.0, + damping=5.0, + actuator_effort_limit=80.0, + ), + "knees": DCMotorCfg( + joint_names_expr=[".*KFE"], + saturation_effort=120.0, + actuator_effort_limit=80.0, + actuator_velocity_limit=7.5, + stiffness=40.0, + damping=5.0, + ), +} + +DELAYED_PD_ACTUATORS = { + "legs": DelayedPDActuatorCfg( + joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], + stiffness=40.0, + damping=5.0, + actuator_effort_limit=80.0, + min_delay=2, + max_delay=4, + ), +} + +IMPLICIT_ONLY_ACTUATORS = { + "legs": ImplicitActuatorCfg( + joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], + stiffness=40.0, + damping=5.0, + ), +} + +CARTPOLE_EXPLICIT_ACTUATORS = { + "all_joints": IdealPDActuatorCfg( + joint_names_expr=["slider_to_cart", "cart_to_pole"], + stiffness=10.0, + damping=1.0, + actuator_effort_limit=100.0, + ), +} + +# --------------------------------------------------------------------------- +# Equivalence assertions +# --------------------------------------------------------------------------- + + +class EquivalenceAssertionsMixin: + """Trajectory/telemetry oracles shared by the Lab-vs-Newton equivalence bases. + + Mixed into each twin's ``_EquivalenceTestBase`` (alongside + ``unittest.TestCase``). The twin's ``setUpClass`` must populate + ``cls.lab_result`` and ``cls.newton_result`` with the dictionaries + returned by its backend-specific simulation runner. + """ + + pos_atol: float = 2e-3 + pos_rtol: float = 1e-3 + vel_atol: float = 1e-2 + vel_rtol: float = 1e-2 + torque_atol: float = 1e-3 + torque_rtol: float = 1e-3 + + def test_joint_positions_match(self): + for step_i, (lab, newton) in enumerate(zip(self.lab_result["joint_pos"], self.newton_result["joint_pos"])): + torch.testing.assert_close( + lab, + newton, + atol=self.pos_atol, + rtol=self.pos_rtol, + msg=f"Joint positions diverged at step {step_i}", + ) + + def test_joint_velocities_match(self): + for step_i, (lab, newton) in enumerate(zip(self.lab_result["joint_vel"], self.newton_result["joint_vel"])): + torch.testing.assert_close( + lab, + newton, + atol=self.vel_atol, + rtol=self.vel_rtol, + msg=f"Joint velocities diverged at step {step_i}", + ) + + def test_applied_effort_match(self): + for step_i, (lab, newton) in enumerate( + zip(self.lab_result["applied_effort"], self.newton_result["applied_effort"]) + ): + torch.testing.assert_close( + lab, + newton, + atol=self.torque_atol, + rtol=self.torque_rtol, + msg=f"applied_effort diverged at step {step_i}", + ) + + def test_computed_effort_match(self): + for step_i, (lab, newton) in enumerate( + zip(self.lab_result["computed_effort"], self.newton_result["computed_effort"]) + ): + torch.testing.assert_close( + lab, + newton, + atol=self.torque_atol, + rtol=self.torque_rtol, + msg=f"computed_effort diverged at step {step_i}", + ) + + +# --------------------------------------------------------------------------- +# Neural network actuator checkpoints +# --------------------------------------------------------------------------- + + +def make_dummy_mlp_checkpoint(device: str = "cpu") -> str: + """Create a minimal TorchScript MLP checkpoint with metadata. + + The network accepts 6 inputs (3 history steps x 2 features per step + in pos_vel order) and outputs 1 effort. + """ + torch.manual_seed(42) + net = ( + torch.nn.Sequential( + torch.nn.Linear(6, 8), + torch.nn.ELU(), + torch.nn.Linear(8, 1), + ) + .to(device) + .eval() + ) + scripted = torch.jit.script(net) + + with tempfile.NamedTemporaryFile(suffix=".pt", delete=False) as tmp: + tmp_path = tmp.name + extra = { + "metadata.json": json.dumps( + { + "model_type": "mlp", + "input_order": "pos_vel", + "input_idx": [0, 1, 2], + "pos_scale": 1.0, + "vel_scale": 0.5, + "torque_scale": 2.0, + } + ) + } + torch.jit.save(scripted, tmp_path, _extra_files=extra) + return tmp_path + + +class _DummyLSTM(torch.nn.Module): + """Minimal LSTM network for actuator testing.""" + + def __init__(self): + super().__init__() + self.lstm = torch.nn.LSTM(input_size=2, hidden_size=4, num_layers=1, batch_first=True) + self.fc = torch.nn.Linear(4, 1) + + def forward( + self, + x: torch.Tensor, + hc: tuple[torch.Tensor, torch.Tensor], + ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]: + out, hc_new = self.lstm(x, hc) + return self.fc(out[:, -1, :]), hc_new + + +def make_dummy_lstm_checkpoint(device: str = "cpu") -> str: + """Create a minimal TorchScript LSTM checkpoint with metadata.""" + torch.manual_seed(42) + net = _DummyLSTM().to(device).eval() + scripted = torch.jit.script(net) + + with tempfile.NamedTemporaryFile(suffix=".pt", delete=False) as tmp: + tmp_path = tmp.name + extra = {"metadata.json": json.dumps({"model_type": "lstm"})} + torch.jit.save(scripted, tmp_path, _extra_files=extra) + return tmp_path + + +# --------------------------------------------------------------------------- +# Domain randomization mocks +# --------------------------------------------------------------------------- + + +class MockScene: + """Minimal stand-in for ``InteractiveScene`` accepted by ``ManagerTermBase``.""" + + def __init__(self, assets: dict, num_envs: int): + self._assets = assets + self.num_envs = num_envs + + def __getitem__(self, name: str): + return self._assets[name] + + +class MockEnv: + """Minimal stand-in for ``ManagerBasedEnv`` for invoking DR terms. + + ``randomize_actuator_gains`` only reads ``env.scene[name]`` and + ``env.scene.num_envs`` (plus ``env.num_envs`` / ``env.device`` from the + ``ManagerTermBase`` properties). No simulator access is needed because + the DR term reaches the actuator adapter through the articulation. + """ + + def __init__(self, assets: dict, num_envs: int, device: str): + self.scene = MockScene(assets, num_envs) + self.num_envs = num_envs + self.device = device + + +def build_dr_term(env, asset_name, joint_ids=None): + """Build a ``randomize_actuator_gains`` event term bound to ``asset_name``.""" + from isaaclab.envs.mdp.events import randomize_actuator_gains # noqa: PLC0415 + from isaaclab.managers import EventTermCfg, SceneEntityCfg # noqa: PLC0415 + + asset_cfg = SceneEntityCfg(asset_name) + if joint_ids is not None: + asset_cfg.joint_ids = joint_ids + cfg = EventTermCfg( + func=randomize_actuator_gains, + params={ + "asset_cfg": asset_cfg, + "stiffness_distribution_params": (100.0, 100.0), + "damping_distribution_params": (5.0, 5.0), + "operation": "abs", + "distribution": "uniform", + }, + ) + return randomize_actuator_gains(cfg, env), asset_cfg + + +# --------------------------------------------------------------------------- +# Per-env reset: actuator state isolation +# --------------------------------------------------------------------------- + + +class ActuatorStateResetBase: + """Reset must clear the actuator state buffers for the requested envs only. + + Inspects ``adapter.actuators[i].state.delay_state.num_pushes`` directly: + + * After warmup, ``num_pushes > 0`` for every DOF (buffer was populated). + * After ``articulation.reset(env_ids=[0])``, the entries for env 0's DOFs + must be ``0`` and the entries for env 1's DOFs must remain ``> 0``. + + Done independently on Lab and Newton paths. Backend-specific twins mix + this into a ``unittest.TestCase`` and provide :meth:`_make_sim_cfg`, + :meth:`_make_articulation`, and :meth:`_get_adapter`. + """ + + RESET_ENV: int = 0 + UNCHANGED_ENV: int = 1 + NUM_ENVS: int = 2 + DT: float = 1.0 / 120.0 + TARGET_OFFSET: float = 0.1 # [rad] added to initial joint positions + RESET_WARMUP_STEPS: int = 3 + + def _make_sim_cfg(self, use_newton_actuators: bool) -> SimulationCfg: + """Return the backend simulation config for the run.""" + raise NotImplementedError + + def _make_articulation(self): + """Construct the backend articulation (DelayedPD on all joints) at ``/World/Env_.*/Robot``.""" + raise NotImplementedError + + def _get_adapter(self, articulation): + """Return the Newton actuator adapter that owns ``articulation``'s actuators.""" + raise NotImplementedError + + def _build_and_warm(self, *, use_newton_actuators: bool): + ctx = build_simulation_context( + device="cuda:0", + gravity_enabled=True, + add_ground_plane=True, + sim_cfg=self._make_sim_cfg(use_newton_actuators), + ) + sim = ctx.__enter__() + sim._app_control_on_stop_handle = None + for i in range(self.NUM_ENVS): + sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) + articulation = self._make_articulation() + sim.reset() + + init_pos = wp.to_torch(articulation.data.joint_pos).clone() + target_pos = init_pos + self.TARGET_OFFSET + target_vel = torch.zeros_like(init_pos) + articulation.set_joint_position_target_index(target=target_pos) + articulation.set_joint_velocity_target_index(target=target_vel) + for _ in range(self.RESET_WARMUP_STEPS): + articulation.write_data_to_sim() + sim.step() + articulation.update(self.DT) + return ctx, sim, articulation + + def test_newton_state_reset_isolated_to_reset_env(self): + """Newton: ``num_pushes`` zeroes for env 0's DOFs only after reset of [0].""" + ctx, sim, articulation = self._build_and_warm(use_newton_actuators=True) + try: + adapter = self._get_adapter(articulation) + self.assertIsNotNone(adapter) + # Find a DelayedPD actuator (it's the only one with delay_state). + stateful_pairs = [ + (act, st) + for act, st in zip(adapter.actuators, adapter._states_a) + if st is not None and getattr(st, "delay_state", None) is not None + ] + self.assertGreater(len(stateful_pairs), 0, "expected at least one DelayedPD actuator with delay_state") + + for act, state in stateful_pairs: + pushes_before = state.delay_state.num_pushes.numpy() + self.assertTrue( + (pushes_before > 0).all(), + "expected non-zero num_pushes for all DOFs after warmup", + ) + + articulation.reset(env_ids=torch.tensor([self.RESET_ENV], device=articulation.device, dtype=torch.long)) + + # Map each entry of ``act.indices`` to its env via the adapter's + # per-env DOF count. On the Newton backend the adapter is model-wide + # (includes free-joint DOFs on floating-base articulations); on + # PhysX it is per-articulation — ``adapter.num_joints`` is the + # correct stride in both cases. + for act, state in stateful_pairs: + pushes_after = state.delay_state.num_pushes.numpy() + indices_np = act.indices.numpy() + for i, global_dof in enumerate(indices_np): + env = int(global_dof) // adapter.num_joints + if env == self.RESET_ENV: + self.assertEqual( + int(pushes_after[i]), + 0, + f"DOF {i} (env {env}) should be reset to 0, got {pushes_after[i]}", + ) + else: + self.assertGreater( + int(pushes_after[i]), + 0, + f"DOF {i} (env {env}) was NOT in reset env_ids but num_pushes is 0", + ) + finally: + ctx.__exit__(None, None, None) + + def test_lab_state_reset_isolated_to_reset_env(self): + """Lab: DelayedPDActuator circular buffer zeroed for env 0 only.""" + ctx, sim, articulation = self._build_and_warm(use_newton_actuators=False) + try: + from isaaclab.actuators import DelayedPDActuator # noqa: PLC0415 + + delayed = [a for a in articulation.actuators.values() if isinstance(a, DelayedPDActuator)] + self.assertGreater(len(delayed), 0, "expected at least one Lab DelayedPDActuator") + actuator = delayed[0] + buf = actuator.positions_delay_buffer._circular_buffer._buffer + # ``_buffer`` shape: (max_length, batch_size, num_joints). + self.assertIsNotNone(buf, "delay buffer should be populated after warmup") + self.assertTrue( + (buf[:, self.UNCHANGED_ENV] != 0).any().item(), + "expected non-zero buffer entries for env 1 after warmup", + ) + + articulation.reset(env_ids=torch.tensor([self.RESET_ENV], device=articulation.device, dtype=torch.long)) + + self.assertTrue( + torch.all(buf[:, self.RESET_ENV] == 0).item(), + f"Lab: env {self.RESET_ENV} buffer not zeroed after reset.", + ) + self.assertTrue( + (buf[:, self.UNCHANGED_ENV] != 0).any().item(), + f"Lab: env {self.UNCHANGED_ENV} buffer was zeroed — reset leaked into an unselected env.", + ) + finally: + ctx.__exit__(None, None, None) diff --git a/source/isaaclab/isaaclab/test/utils/articulation_ordering.py b/source/isaaclab/isaaclab/test/utils/articulation_ordering.py index ae66906bb458..25dbf4024419 100644 --- a/source/isaaclab/isaaclab/test/utils/articulation_ordering.py +++ b/source/isaaclab/isaaclab/test/utils/articulation_ordering.py @@ -60,17 +60,15 @@ _ORDERING_TRACE_FIELDS = ( "joint_pos", "joint_vel", - "computed_torque", - "applied_torque", - "adapter_computed_effort", + "computed_effort", + "applied_effort", "adapter_applied_effort", ) _ORDERING_TRACE_TOLERANCES = { "joint_pos": (2e-3, 1e-3), "joint_vel": (1e-2, 1e-2), - "computed_torque": (1e-3, 1e-3), - "applied_torque": (1e-3, 1e-3), - "adapter_computed_effort": (1e-3, 1e-3), + "computed_effort": (1e-3, 1e-3), + "applied_effort": (1e-3, 1e-3), "adapter_applied_effort": (1e-3, 1e-3), } diff --git a/source/isaaclab/isaaclab/utils/leapp/export_annotator.py b/source/isaaclab/isaaclab/utils/leapp/export_annotator.py index 7942369ace0d..a5c645d826be 100644 --- a/source/isaaclab/isaaclab/utils/leapp/export_annotator.py +++ b/source/isaaclab/isaaclab/utils/leapp/export_annotator.py @@ -42,6 +42,7 @@ from leapp import annotate from leapp.utils.tensor_description import TensorSemantics +from isaaclab.actuators import IdealPDActuator, ImplicitActuator from isaaclab.assets.articulation.base_articulation import BaseArticulation from isaaclab.managers import ManagerTermBase from isaaclab.utils.array import convert_to_torch @@ -82,6 +83,8 @@ def _effective_joint_gains(real_asset) -> tuple[torch.Tensor | None, torch.Tenso kp = stiffness.torch.clone() if stiffness is not None else None kd = damping.torch.clone() if damping is not None else None for actuator in getattr(real_asset, "actuators", {}).values(): + if not isinstance(actuator, (ImplicitActuator, IdealPDActuator)): + continue if kp is not None: kp[:, actuator.joint_indices] = actuator.stiffness if kd is not None: diff --git a/source/isaaclab/isaaclab/utils/string.py b/source/isaaclab/isaaclab/utils/string.py index fa83688154d6..c0419bd9f8dd 100644 --- a/source/isaaclab/isaaclab/utils/string.py +++ b/source/isaaclab/isaaclab/utils/string.py @@ -502,6 +502,24 @@ def resolve_matching_names_values( return index_list, names_list, values_list +def _resolve_matching_values_dense(value: dict[str, float | int] | float | int, names: list[str]) -> tuple[float, ...]: + """Expand a scalar or regex-keyed mapping into dense per-name float values. + + Scalars broadcast to every name. Mapping entries resolve through + :func:`resolve_matching_names_values`; names not matched by any pattern + resolve to zero. This zero fill is the shared contract for actuator + configuration values across model parsing, alias comparison, and USD + authoring. + """ + if isinstance(value, (float, int)): + return (float(value),) * len(names) + indices, _, values = resolve_matching_names_values(value, names) + resolved_values = [0.0] * len(names) + for index, resolved_value in zip(indices, values, strict=True): + resolved_values[index] = float(resolved_value) + return tuple(resolved_values) + + def find_unique_string_name(initial_name: str, is_unique_fn: Callable[[str], bool]) -> str: """Find a unique string name based on the predicate function provided. The string is appended with "_N", where N is a natural number till the resultant string diff --git a/source/isaaclab/isaaclab/utils/warp/launch_cache.py b/source/isaaclab/isaaclab/utils/warp/launch_cache.py index b2d50a91fac6..5feab6de0d3f 100644 --- a/source/isaaclab/isaaclab/utils/warp/launch_cache.py +++ b/source/isaaclab/isaaclab/utils/warp/launch_cache.py @@ -32,5 +32,9 @@ def launch(self, key: object, kernel: wp.Kernel, *, dim, inputs, outputs) -> Non self._commands[key] = command command.launch() - def clear(self) -> None: - self._commands.clear() + def clear(self, key: object | None = None) -> None: + """Remove one recorded launch or every recorded launch.""" + if key is None: + self._commands.clear() + else: + self._commands.pop(key, None) diff --git a/source/isaaclab/isaaclab/utils/wrench_composer.py b/source/isaaclab/isaaclab/utils/wrench_composer.py index 37c6137c533e..31d08089160c 100644 --- a/source/isaaclab/isaaclab/utils/wrench_composer.py +++ b/source/isaaclab/isaaclab/utils/wrench_composer.py @@ -549,9 +549,9 @@ def reset( ): """Reset the wrench composer buffers. - With no arguments, zeros all seven buffers (5 input + 2 output) and clears all flags. - With ``env_ids`` or ``env_mask``, performs a partial reset on the specified environments - using the reset kernels. + With no selection or ``env_ids=slice(None)``, zeros all seven buffers (5 input + 2 output) and clears all + flags. Other ``env_ids`` or ``env_mask`` values perform a partial reset on the specified environments using + the reset kernels. .. caution:: If both ``env_ids`` and ``env_mask`` are provided, ``env_mask`` takes precedence. @@ -559,7 +559,8 @@ def reset( env_ids: Environment indices. Defaults to None (all environments). env_mask: Environment mask. Defaults to None (all environments). """ - if env_ids is None and env_mask is None: + full_reset = env_mask is None and (env_ids is None or (isinstance(env_ids, slice) and env_ids == slice(None))) + if full_reset: # Full reset: zero all 7 buffers self._global_force_w.zero_() self._global_torque_w.zero_() @@ -589,9 +590,7 @@ def reset( self._dirty = True else: # Partial reset via index - if env_ids is None or env_ids == slice(None): - env_ids = self._ALL_ENV_INDICES - elif isinstance(env_ids, list): + if isinstance(env_ids, list): env_ids = wp.array(env_ids, dtype=wp.int32, device=self.device) wp.launch( diff --git a/source/isaaclab/test/actuators/test_actuator_collection.py b/source/isaaclab/test/actuators/test_actuator_collection.py new file mode 100644 index 000000000000..3194bf099cb1 --- /dev/null +++ b/source/isaaclab/test/actuators/test_actuator_collection.py @@ -0,0 +1,1106 @@ +# 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 + +"""Tests for the actuator collection runtime.""" + +from __future__ import annotations + +import re +import warnings +from collections.abc import Sequence +from types import SimpleNamespace + +import pytest +import torch +import warp as wp + +import isaaclab.actuators as actuator_api +from isaaclab.actuators import ( + ActuatorCollection, + ActuatorControl, + DCMotor, + DCMotorCfg, + DelayedPDActuatorCfg, + IdealPDActuator, + IdealPDActuatorCfg, + ImplicitActuator, + ImplicitActuatorCfg, +) +from isaaclab.actuators.actuator_control import ArticulationActuatorControl +from isaaclab.actuators.newton import read_group_parameter, write_group_parameter +from isaaclab.utils.warp import ProxyArray + + +def _implicit_cfg(**kwargs) -> ImplicitActuatorCfg: + """Create a valid implicit actuator config for collection tests.""" + return ImplicitActuatorCfg(joint_names_expr=[".*"], stiffness=0.0, damping=0.0, **kwargs) + + +class SelectorRecordingActuator(ImplicitActuator): + """Custom actuator that records the selector supplied to :meth:`compute`.""" + + def compute(self, control_action, joint_pos, joint_vel): + self.observed_joint_indices = control_action.joint_indices + return super().compute(control_action, joint_pos, joint_vel) + + +def _ideal_cfg(joints: list[str], *, stiffness: float, damping: float, effort_limit: float): + return IdealPDActuatorCfg( + joint_names_expr=joints, + stiffness=stiffness, + damping=damping, + actuator_effort_limit=effort_limit, + actuator_velocity_limit=100.0, + ) + + +def _ideal_pd_cfg(**kwargs) -> IdealPDActuatorCfg: + """Create a minimal ideal PD actuator configuration.""" + return IdealPDActuatorCfg(joint_names_expr=[".*"], stiffness=0.0, damping=0.0, **kwargs) + + +def _dc_cfg( + joints: list[str], + *, + stiffness: float, + damping: float, + effort_limit: float, + velocity_limit: float, + saturation_effort: float, +): + return DCMotorCfg( + joint_names_expr=joints, + stiffness=stiffness, + damping=damping, + actuator_effort_limit=effort_limit, + actuator_velocity_limit=velocity_limit, + saturation_effort=saturation_effort, + ) + + +def _assign_deterministic_inputs(collection: ActuatorCollection, control: FakeActuatorControl) -> None: + control.joint_pos.torch.copy_( + torch.tensor( + [ + [0.35, -0.80, 1.25, -1.60], + [-0.45, 0.95, -1.35, 1.80], + ], + dtype=torch.float32, + ) + ) + control.joint_vel.torch.copy_( + torch.tensor( + [ + [16.0, 31.0, -17.0, -32.0], + [-18.0, -33.0, 19.0, 34.0], + ], + dtype=torch.float32, + ) + ) + collection.target_command.position.torch.copy_( + torch.tensor( + [ + [1.40, -0.20, -0.75, 2.20], + [0.15, -1.45, 2.05, -0.65], + ], + dtype=torch.float32, + ) + ) + collection.target_command.velocity.torch.copy_( + torch.tensor( + [ + [-3.5, 4.25, 5.75, -6.5], + [7.0, -8.5, -9.25, 10.75], + ], + dtype=torch.float32, + ) + ) + collection.target_command.effort.torch.copy_( + torch.tensor( + [ + [2.25, -3.50, 4.75, -5.25], + [-6.50, 7.75, -8.25, 9.50], + ], + dtype=torch.float32, + ) + ) + + +class FakeActuatorControl(ActuatorControl): + """Small backend-neutral control object used by collection unit tests.""" + + def __init__(self, *, num_envs: int = 2, joint_names: list[str] | None = None, device: str = "cpu"): + self._num_instances = num_envs + self._joint_names = joint_names or ["joint_0", "joint_1", "joint_2"] + self._device = device + self._joint_pos = ProxyArray(wp.zeros((num_envs, len(self._joint_names)), dtype=wp.float32, device=device)) + self._joint_vel = ProxyArray(wp.zeros((num_envs, len(self._joint_names)), dtype=wp.float32, device=device)) + shape = (num_envs, len(self._joint_names)) + self._joint_stiffness = ProxyArray(wp.zeros(shape, dtype=wp.float32, device=device)) + self._joint_damping = ProxyArray(wp.zeros(shape, dtype=wp.float32, device=device)) + self._joint_effort_limits = ProxyArray(wp.full(shape, 100.0, dtype=wp.float32, device=device)) + zeros = torch.zeros(shape, dtype=torch.float32, device=device) + ones = torch.ones(shape, dtype=torch.float32, device=device) + self._current_joint_properties = { + "stiffness": self._joint_stiffness.torch, + "damping": self._joint_damping.torch, + "armature": zeros.clone(), + "friction": zeros.clone(), + "dynamic_friction": zeros.clone(), + "viscous_friction": zeros.clone(), + "joint_effort_limit": self._joint_effort_limits.torch, + "joint_velocity_limit": ones * 10.0, + } + self.written_properties: list[tuple[dict[str, torch.Tensor], torch.Tensor | slice, bool, bool]] = [] + self.staged_commands: list[str] = [] + self.submitted = False + + @property + def num_instances(self) -> int: + return self._num_instances + + @property + def num_joints(self) -> int: + return len(self._joint_names) + + @property + def num_fixed_tendons(self) -> int: + return 0 + + @property + def device(self) -> str: + return self._device + + @property + def joint_pos(self) -> ProxyArray: + return self._joint_pos + + @property + def joint_vel(self) -> ProxyArray: + return self._joint_vel + + @property + def joint_stiffness(self) -> ProxyArray: + return self._joint_stiffness + + @property + def joint_damping(self) -> ProxyArray: + return self._joint_damping + + @property + def joint_effort_limits(self) -> ProxyArray: + return self._joint_effort_limits + + def find_joints(self, name_keys: str | Sequence[str]) -> tuple[list[int], list[str]]: + expressions = [name_keys] if isinstance(name_keys, str) else list(name_keys) + matches = [ + (joint_id, joint_name) + for joint_id, joint_name in enumerate(self._joint_names) + if any(re.fullmatch(expression, joint_name) for expression in expressions) + ] + return [joint_id for joint_id, _ in matches], [joint_name for _, joint_name in matches] + + def resolve_env_ids(self, env_ids: Sequence[int] | torch.Tensor | wp.array | None) -> torch.Tensor | wp.array: + if env_ids is None: + return wp.array(list(range(self.num_instances)), dtype=wp.int32, device=self.device) + if isinstance(env_ids, torch.Tensor | wp.array): + return env_ids + return wp.array(list(env_ids), dtype=wp.int32, device=self.device) + + def resolve_joint_ids(self, joint_ids: Sequence[int] | torch.Tensor | wp.array | None) -> torch.Tensor | wp.array: + if joint_ids is None: + return wp.array(list(range(self.num_joints)), dtype=wp.int32, device=self.device) + if isinstance(joint_ids, torch.Tensor | wp.array): + return joint_ids + return wp.array(list(joint_ids), dtype=wp.int32, device=self.device) + + def assert_shape_and_dtype( + self, tensor: torch.Tensor | wp.array | float, shape: tuple[int, ...], dtype: type, name: str + ) -> None: + if isinstance(tensor, (float, int)): + return + if isinstance(tensor, torch.Tensor): + assert tuple(tensor.shape) == shape + return + assert tensor.shape == shape + assert tensor.dtype == dtype + + def assert_shape_and_dtype_mask( + self, tensor: torch.Tensor | wp.array | float, masks: tuple[wp.array, ...], dtype: type, name: str + ) -> None: + self.assert_shape_and_dtype(tensor, tuple(mask.shape[0] for mask in masks), dtype, name) + + def get_default_joint_properties(self, joint_ids: torch.Tensor | wp.array | slice) -> dict[str, torch.Tensor]: + if isinstance(joint_ids, wp.array): + joint_ids = wp.to_torch(joint_ids).to(device=self.device, dtype=torch.long) + properties = self._current_joint_properties + return { + "stiffness": self.joint_stiffness.torch[:, joint_ids].clone(), + "damping": self.joint_damping.torch[:, joint_ids].clone(), + "armature": properties["armature"][:, joint_ids].clone(), + "friction": properties["friction"][:, joint_ids].clone(), + "dynamic_friction": properties["dynamic_friction"][:, joint_ids].clone(), + "viscous_friction": properties["viscous_friction"][:, joint_ids].clone(), + "joint_effort_limit": self.joint_effort_limits.torch[:, joint_ids].clone(), + "joint_velocity_limit": properties["joint_velocity_limit"][:, joint_ids].clone(), + } + + def write_resolved_joint_properties( + self, + properties: dict[str, torch.Tensor], + joint_ids: torch.Tensor | slice, + *, + implicit: bool, + native_managed: bool, + ) -> None: + self.written_properties.append((properties, joint_ids, implicit, native_managed)) + self.joint_effort_limits.torch[:, joint_ids] = properties["joint_effort_limit"] + if implicit and not native_managed: + self.joint_stiffness.torch[:, joint_ids] = properties["stiffness"] + self.joint_damping.torch[:, joint_ids] = properties["damping"] + else: + self.joint_stiffness.torch[:, joint_ids] = 0.0 + self.joint_damping.torch[:, joint_ids] = 0.0 + + def stage_user_command( + self, + command_name: str, + collection: ActuatorCollection, + env_ids: torch.Tensor | wp.array | None, + joint_ids: torch.Tensor | wp.array | None, + env_mask: wp.array | None, + joint_mask: wp.array | None, + ) -> None: + self.staged_commands.append(command_name) + + def submit_commands(self, collection: ActuatorCollection) -> None: + self.submitted = True + + +class _FakeNewtonActuator: + """Newton-actuator stand-in; a plain class so the view's mapping cache can hash it.""" + + def __init__(self, controller, indices): + self.controller = controller + self.delay = None + self.clamping = [] + self.indices = indices + + +class NativeFakeActuatorControl(FakeActuatorControl): + """Control object that handles actuator execution natively.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + size = self.num_instances * self.num_joints + self.newton_actuator = _FakeNewtonActuator( + controller=SimpleNamespace(), + indices=wp.array(list(range(size)), dtype=wp.uint32, device=self.device), + ) + + @property + def native_actuator_path_active(self) -> bool: + return True + + def prepare_native_actuators(self, collection, actuator_cfgs) -> set[str]: + self.prepared_actuator_cfgs = actuator_cfgs + return set(actuator_cfgs) + + def compute_native_actuators(self, collection: ActuatorCollection, dt: float) -> bool: + return True + + def finalize_native_actuators(self, collection): + from isaaclab.actuators.newton.adapter import LightArticulationView, NewtonActuatorSelection + + return NewtonActuatorSelection( + view=LightArticulationView(self.num_instances, self.num_joints, self.device), + actuators=[self.newton_actuator], + ) + + +class NativeGainFakeActuatorControl(NativeFakeActuatorControl): + """Native control backed by one Newton-shaped actuator with controller-owned storage.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + size = self.num_instances * self.num_joints + self.newton_actuator.controller = SimpleNamespace( + kp=wp.zeros(size, dtype=wp.float32, device=self.device), + kd=wp.zeros(size, dtype=wp.float32, device=self.device), + ) + + @property + def native_gains(self) -> dict[str, torch.Tensor]: + """Live torch views over the controller-owned gain storage.""" + shape = (self.num_instances, self.num_joints) + controller = self.newton_actuator.controller + return { + attr: wp.to_torch(getattr(controller, attr)).view(shape) + for attr in ("kp", "kd") + if hasattr(controller, attr) + } + + +@pytest.mark.parametrize( + ("cfg_factory", "with_joint_limit", "expected_joint_limit"), + [ + (_ideal_pd_cfg, True, 34.0), + (_implicit_cfg, True, 34.0), + (_implicit_cfg, False, 12.0), + ], +) +def test_deprecated_effort_limit_forwards_by_actuator_type(cfg_factory, with_joint_limit, expected_joint_limit): + """``effort_limit`` resolves to the rated ``actuator_effort_limit`` for every actuator type. + + For implicit groups without a separate solver clamp, the rated value also reaches + ``joint_effort_limit`` for backward compatibility. + """ + cfg = ( + cfg_factory(effort_limit=12.0, joint_effort_limit=34.0) if with_joint_limit else cfg_factory(effort_limit=12.0) + ) + # Lab execution keeps the group as an inspectable Lab model instance; the explicit + # variant additionally warns about deprecated Lab execution, which pytest.warns tolerates. + control = FakeActuatorControl() + + with pytest.warns(DeprecationWarning, match="actuator_effort_limit"): + collection = ActuatorCollection({"motor": cfg}, control) + + # Runtime alias-property behavior (group.effort_limit get/set warnings) is covered by + # the per-actuator suites in test_implicit_actuator.py and test_ideal_pd_actuator.py. + group = collection["motor"] + assert group.cfg.actuator_effort_limit == 12.0 + assert group.cfg.joint_effort_limit == expected_joint_limit + torch.testing.assert_close(group.actuator_effort_limit, torch.full((2, 3), 12.0)) + if isinstance(group, ImplicitActuator): + # Lab explicit models hold no solver-limit view; the solver clamp lives on articulation data. + torch.testing.assert_close(group.joint_effort_limit, torch.full((2, 3), expected_joint_limit)) + + +def test_constructor_resolves_deprecated_velocity_limit_alias(): + cfg = _ideal_pd_cfg(velocity_limit_sim=34.0) + + with pytest.warns(DeprecationWarning, match="joint_velocity_limit"): + actuator = IdealPDActuator( + cfg, + joint_names=["joint_0", "joint_1", "joint_2"], + joint_ids=slice(None), + num_envs=2, + device="cpu", + ) + + assert actuator.cfg.joint_velocity_limit == 34.0 + assert actuator.cfg.velocity_limit_sim is None + + +def test_implicit_actuator_separate_rated_effort_limit_is_honored(): + """A rated limit distinct from the solver clamp stays on the implicit actuator.""" + cfg = _implicit_cfg(actuator_effort_limit=12.0, joint_effort_limit=34.0) + + collection = ActuatorCollection({"motor": cfg}, FakeActuatorControl()) + + group = collection["motor"] + torch.testing.assert_close(group.actuator_effort_limit, torch.full((2, 3), 12.0)) + torch.testing.assert_close(group.joint_effort_limit, torch.full((2, 3), 34.0)) + + +@pytest.mark.parametrize( + ("cfg", "actuator_type", "canonical_name"), + [ + (_ideal_pd_cfg(), IdealPDActuator, "actuator_effort_limit"), + (_implicit_cfg(), ImplicitActuator, "joint_effort_limit"), + ], +) +def test_constructor_effort_limit_alias_conflicts_with_explicit_infinity(cfg, actuator_type, canonical_name): + constructor_kwargs = { + "joint_names": ["joint_0", "joint_1", "joint_2"], + "joint_ids": slice(None), + "num_envs": 2, + "device": "cpu", + "stiffness": 0.0, + "damping": 0.0, + } + + with pytest.warns(DeprecationWarning, match=canonical_name): + with pytest.raises(ValueError, match=rf"conflicting {canonical_name}.*effort_limit"): + actuator_type( + cfg, + **constructor_kwargs, + **{canonical_name: torch.inf, "effort_limit": 12.0}, + ) + + with pytest.warns(DeprecationWarning, match=canonical_name): + actuator = actuator_type( + cfg.copy(), + **constructor_kwargs, + **{canonical_name: torch.full((2, 3), 12.0), "effort_limit": 12.0}, + ) + torch.testing.assert_close(getattr(actuator, canonical_name), torch.full((2, 3), 12.0)) + + +def test_equivalent_limit_aliases_prefer_canonical_values(): + # Rows with a canonical value assert the canonical value wins over an equivalent alias. + # Rows with canonical_value=None assert the deprecated *_sim alias forwards onto the + # prepared copy when the canonical field is unset, without mutating the user's cfg. + scenarios = ( + ("joint_effort_limit", "effort_limit_sim", 12.0, {"joint_.*": 12.0}, _ideal_pd_cfg, NativeFakeActuatorControl), + ( + "joint_velocity_limit", + "velocity_limit_sim", + 34.0, + {"joint_.*": 34.0}, + _ideal_pd_cfg, + NativeFakeActuatorControl, + ), + ("actuator_effort_limit", "effort_limit", {"joint_.*": 12.0}, 12.0, _ideal_pd_cfg, NativeFakeActuatorControl), + ("joint_effort_limit", "effort_limit", {"joint_.*": 12.0}, 12.0, _implicit_cfg, FakeActuatorControl), + ("joint_effort_limit", "effort_limit_sim", None, 12.0, _ideal_pd_cfg, NativeFakeActuatorControl), + ("joint_velocity_limit", "velocity_limit_sim", None, 34.0, _ideal_pd_cfg, NativeFakeActuatorControl), + ) + + for canonical_name, alias_name, canonical_value, alias_value, cfg_factory, control_factory in scenarios: + cfg_kwargs = {alias_name: alias_value} + if canonical_value is not None: + cfg_kwargs[canonical_name] = canonical_value + cfg = cfg_factory(**cfg_kwargs) + control = control_factory() + with pytest.warns(DeprecationWarning): + collection = ActuatorCollection({"motor": cfg}, control) + + prepared_cfgs = getattr(control, "prepared_actuator_cfgs", None) + resolved_cfg = prepared_cfgs["motor"] if prepared_cfgs is not None else collection["motor"].cfg + expected_value = canonical_value if canonical_value is not None else alias_value + assert getattr(resolved_cfg, canonical_name) == expected_value + # the user's cfg is never mutated: the canonical field keeps its original value. + assert getattr(cfg, canonical_name) == canonical_value + assert getattr(cfg, alias_name) == alias_value + if canonical_value is None: + assert resolved_cfg is not cfg + + +def test_conflicting_limit_aliases_raise(): + scenarios = ( + ("joint_effort_limit", "effort_limit_sim", _ideal_pd_cfg, NativeFakeActuatorControl), + ("joint_velocity_limit", "velocity_limit_sim", _ideal_pd_cfg, NativeFakeActuatorControl), + ("actuator_effort_limit", "effort_limit", _ideal_pd_cfg, NativeFakeActuatorControl), + ("actuator_effort_limit", "effort_limit", _implicit_cfg, FakeActuatorControl), + ) + + for canonical_name, alias_name, cfg_factory, control_factory in scenarios: + cfg = cfg_factory( + **{ + canonical_name: {"joint_0": 12.0, "joint_1": 10.0, "joint_2": 8.0}, + alias_name: {"joint_0": 12.0, "joint_1": 11.0, "joint_2": 8.0}, + } + ) + with ( + pytest.warns(DeprecationWarning), + pytest.raises(ValueError, match=rf"motor.*{canonical_name}.*{alias_name}"), + ): + ActuatorCollection({"motor": cfg}, control_factory()) + + +class ProxyFinderActuatorControl(FakeActuatorControl): + """Control object whose joint finder returns cached proxy indices.""" + + def find_joints(self, name_keys: str | Sequence[str]) -> tuple[ProxyArray, list[str]]: + return ProxyArray(wp.array([0, 2], dtype=wp.int32, device=self.device)), ["joint_0", "joint_2"] + + +class FakeArticulationActuatorControl(ArticulationActuatorControl): + """Concrete shared articulation-control test adapter.""" + + def submit_commands(self, collection: ActuatorCollection) -> None: + pass + + +class FakeArticulation: + """Small articulation facade for shared control tests.""" + + def __init__(self): + self.num_instances = 2 + self.num_joints = 3 + self.num_fixed_tendons = 0 + self.device = "cpu" + shape = (self.num_instances, self.num_joints) + zeros = torch.zeros(shape, dtype=torch.float32) + ones = torch.ones(shape, dtype=torch.float32) + self.data = SimpleNamespace( + joint_pos=ProxyArray(wp.zeros(shape, dtype=wp.float32, device=self.device)), + joint_vel=ProxyArray(wp.zeros(shape, dtype=wp.float32, device=self.device)), + joint_stiffness=ProxyArray(wp.zeros(shape, dtype=wp.float32, device=self.device)), + joint_damping=ProxyArray(wp.zeros(shape, dtype=wp.float32, device=self.device)), + joint_armature=SimpleNamespace(torch=zeros.clone()), + joint_friction_coeff=SimpleNamespace(torch=zeros.clone()), + joint_effort_limits=ProxyArray(wp.full(shape, 100.0, dtype=wp.float32, device=self.device)), + joint_vel_limits=SimpleNamespace(torch=ones * 10.0), + ) + self.calls: list[tuple[str, dict]] = [] + self.resolved_env_ids: list[object] = [] + self.resolved_joint_ids: list[object] = [] + + def find_joints( + self, name_keys: str | Sequence[str], *, as_proxy: bool = False + ) -> tuple[list[int] | ProxyArray, list[str]]: + expressions = [name_keys] if isinstance(name_keys, str) else list(name_keys) + matches = [ + (joint_id, joint_name) + for joint_id, joint_name in enumerate(["joint_0", "joint_1", "joint_2"]) + if any(re.fullmatch(expression, joint_name) for expression in expressions) + ] + joint_ids = [joint_id for joint_id, _ in matches] + joint_names = [joint_name for _, joint_name in matches] + if as_proxy: + resolved_ids = ProxyArray(wp.array(joint_ids, dtype=wp.int32, device=self.device)) + else: + resolved_ids = joint_ids + return resolved_ids, joint_names + + def _resolve_env_ids(self, env_ids: Sequence[int] | torch.Tensor | wp.array | None) -> wp.array: + self.resolved_env_ids.append(env_ids) + values = list(range(self.num_instances)) if env_ids is None else list(env_ids) + return wp.array(values, dtype=wp.int32, device=self.device) + + def _resolve_joint_ids(self, joint_ids: Sequence[int] | torch.Tensor | wp.array | None) -> wp.array: + self.resolved_joint_ids.append(joint_ids) + values = list(range(self.num_joints)) if joint_ids is None else list(joint_ids) + return wp.array(values, dtype=wp.int32, device=self.device) + + def assert_shape_and_dtype( + self, tensor: torch.Tensor | wp.array | float, shape: tuple[int, ...], dtype: type, name: str + ) -> None: + pass + + def assert_shape_and_dtype_mask( + self, tensor: torch.Tensor | wp.array | float, masks: tuple[wp.array, ...], dtype: type, name: str + ) -> None: + pass + + def write_joint_effort_limit_to_sim_index(self, **kwargs) -> None: + self.calls.append(("effort_limit", kwargs)) + + def write_joint_velocity_limit_to_sim_index(self, **kwargs) -> None: + self.calls.append(("velocity_limit", kwargs)) + + def write_joint_armature_to_sim_index(self, **kwargs) -> None: + self.calls.append(("armature", kwargs)) + + def write_joint_friction_coefficient_to_sim_index(self, **kwargs) -> None: + self.calls.append(("friction", kwargs)) + + def write_joint_dynamic_friction_coefficient_to_sim_index(self, **kwargs) -> None: + self.calls.append(("dynamic_friction", kwargs)) + + def write_joint_viscous_friction_coefficient_to_sim_index(self, **kwargs) -> None: + self.calls.append(("viscous_friction", kwargs)) + + def write_joint_stiffness_to_sim_index(self, **kwargs) -> None: + self.calls.append(("stiffness", kwargs)) + self.data.joint_stiffness.torch[:, kwargs["joint_ids"]] = kwargs["stiffness"] + + def write_joint_damping_to_sim_index(self, **kwargs) -> None: + self.calls.append(("damping", kwargs)) + self.data.joint_damping.torch[:, kwargs["joint_ids"]] = kwargs["damping"] + + +def test_articulation_control_provides_common_forwarding_and_property_writes(): + articulation = FakeArticulation() + control = FakeArticulationActuatorControl(articulation) + + assert not FakeActuatorControl().native_actuator_path_active + assert not control.native_actuator_path_active + control._native_actuator_path_active = True + assert control.native_actuator_path_active + + assert control.num_instances == articulation.num_instances + assert control.num_joints == articulation.num_joints + assert control.device == articulation.device + assert control.joint_stiffness is articulation.data.joint_stiffness + assert control.joint_damping is articulation.data.joint_damping + assert control.joint_effort_limits is articulation.data.joint_effort_limits + + defaults = control.get_default_joint_properties(slice(None)) + torch.testing.assert_close(defaults["dynamic_friction"], torch.zeros(2, 3)) + torch.testing.assert_close(defaults["viscous_friction"], torch.zeros(2, 3)) + + properties = { + "joint_effort_limit": torch.ones((2, 3)), + "joint_velocity_limit": torch.ones((2, 3)) * 2.0, + "armature": torch.ones((2, 3)) * 3.0, + "friction": torch.ones((2, 3)) * 4.0, + "dynamic_friction": torch.zeros((2, 3)), + "viscous_friction": torch.zeros((2, 3)), + "stiffness": torch.ones((2, 3)) * 5.0, + "damping": torch.ones((2, 3)) * 6.0, + } + + control.write_resolved_joint_properties(properties, slice(None), implicit=False, native_managed=False) + + # the write order is not a contract: compare the set of property writes order-insensitively. + assert sorted(name for name, _ in articulation.calls) == sorted( + ["effort_limit", "velocity_limit", "armature", "friction", "stiffness", "damping"] + ) + calls_by_name = dict(articulation.calls) + assert calls_by_name["stiffness"]["stiffness"] == 0.0 + assert calls_by_name["damping"]["damping"] == 0.0 + control.resolve_env_ids((1,)) + control.resolve_joint_ids(range(1, 3)) + assert articulation.resolved_env_ids == [[1]] + assert articulation.resolved_joint_ids == [[1, 2]] + + +def test_articulation_control_projects_warp_joint_property_selectors(): + articulation = FakeArticulation() + control = FakeArticulationActuatorControl(articulation) + articulation.data.joint_armature.torch.copy_( + torch.tensor( + [ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + ] + ) + ) + joint_ids = wp.array([2, 0], dtype=wp.int32, device="cpu") + + defaults = control.get_default_joint_properties(joint_ids) + + expected = torch.tensor( + [ + [3.0, 1.0], + [6.0, 4.0], + ] + ) + torch.testing.assert_close(defaults["armature"], expected) + + +def test_native_explicit_groups_zero_solver_drives_and_build_no_lab_model(monkeypatch): + """Zero the solver drives of a native explicit group and expose the Newton actuator.""" + from isaaclab.actuators.newton.adapter import LightArticulationView, NewtonActuatorSelection + + articulation = FakeArticulation() + articulation.data.joint_stiffness.torch.fill_(17.0) + articulation.data.joint_damping.torch.fill_(3.0) + control = FakeArticulationActuatorControl(articulation) + newton_actuator = _FakeNewtonActuator( + controller=SimpleNamespace(), + indices=wp.array( + list(range(articulation.num_instances * articulation.num_joints)), + dtype=wp.uint32, + device=articulation.device, + ), + ) + monkeypatch.setattr(control, "prepare_native_actuators", lambda collection, cfgs: set(cfgs)) + monkeypatch.setattr(control, "_native_actuator_path_active", True) + monkeypatch.setattr( + control, + "finalize_native_actuators", + lambda collection: NewtonActuatorSelection( + view=LightArticulationView(articulation.num_instances, articulation.num_joints, articulation.device), + actuators=[newton_actuator], + ), + ) + + collection = ActuatorCollection( + { + "explicit": IdealPDActuatorCfg( + joint_names_expr=[".*"], + stiffness=None, + damping=None, + actuator_effort_limit=100.0, + actuator_velocity_limit=10.0, + ) + }, + control, + ) + + # No Lab model is built for the Newton-executed group: the owner is exposed directly. + assert collection["explicit"] is newton_actuator + torch.testing.assert_close(articulation.data.joint_stiffness.torch, torch.zeros((2, 3))) + torch.testing.assert_close(articulation.data.joint_damping.torch, torch.zeros((2, 3))) + assert articulation.calls[-2][1]["stiffness"] == 0.0 + assert articulation.calls[-1][1]["damping"] == 0.0 + + +def test_native_group_parameters_route_through_the_collection_door(): + """Read and write native group parameters through the collection's single parameter door.""" + control = NativeGainFakeActuatorControl() + control.native_gains["kp"].copy_(torch.tensor([[2.0, 3.0, 4.0], [5.0, 6.0, 7.0]])) + control.native_gains["kd"].copy_(torch.tensor([[0.2, 0.3, 0.4], [0.5, 0.6, 0.7]])) + + collection = ActuatorCollection( + {"native": _ideal_cfg([".*"], stiffness=11.0, damping=1.1, effort_limit=100.0)}, control + ) + + # Reads are live projections of the controller-owned storage. + torch.testing.assert_close( + read_group_parameter(collection, "native", "controller", "kp"), control.native_gains["kp"] + ) + control.native_gains["kd"][1, 2] = 1.7 + assert read_group_parameter(collection, "native", "controller", "kd")[1, 2] == 1.7 + + # The group's mapping entry is the owning Newton actuator: no stale Lab mirrors exist, + # and direct modification of the controller storage is observed by the door reads. + group = collection["native"] + assert group is control.newton_actuator + wp.to_torch(group.controller.kp).view(2, 3)[0, 0] = 21.0 + assert read_group_parameter(collection, "native", "controller", "kp")[0, 0] == 21.0 + wp.to_torch(group.controller.kp).view(2, 3)[0, 0] = 2.0 + + # The single write path patches the controller storage in place over an env/joint selection. + write_group_parameter( + collection, + "native", + "controller", + "kp", + values=torch.tensor([[42.0]]), + env_ids=torch.tensor([0]), + joint_ids=torch.tensor([1]), + ) + torch.testing.assert_close(control.native_gains["kp"], torch.tensor([[2.0, 42.0, 4.0], [5.0, 6.0, 7.0]])) + write_group_parameter(collection, "native", "controller", "kd", values=torch.full((2, 3), 0.9)) + torch.testing.assert_close(control.native_gains["kd"], torch.full((2, 3), 0.9)) + + with pytest.raises(ValueError, match=r"No Newton actuator exposes parameter \('controller', 'kq'\)"): + write_group_parameter(collection, "native", "controller", "kq", values=torch.zeros((2, 3))) + with pytest.raises(ValueError, match=r"Unknown actuator component 'gains'"): + read_group_parameter(collection, "native", "gains", "kp") + + # A parameter the controllers do not expose raises instead of falling back to stale values. + unsupported_control = NativeGainFakeActuatorControl() + del unsupported_control.newton_actuator.controller.kd + unsupported = ActuatorCollection( + {"native": _ideal_cfg([".*"], stiffness=11.0, damping=1.1, effort_limit=100.0)}, unsupported_control + ) + torch.testing.assert_close(read_group_parameter(unsupported, "native", "controller", "kp"), torch.zeros((2, 3))) + with pytest.raises(ValueError, match=r"No Newton actuator exposes parameter \('controller', 'kd'\)"): + read_group_parameter(unsupported, "native", "controller", "kd") + + # Groups that are not Newton-managed keep plain construction gains, and the door rejects them. + plain = ActuatorCollection( + {"plain": _ideal_cfg([".*"], stiffness=11.0, damping=1.1, effort_limit=100.0)}, FakeActuatorControl() + ) + torch.testing.assert_close(plain["plain"].stiffness, torch.full((2, 3), 11.0)) + with pytest.raises(ValueError, match=r"'plain' is not executed by Newton actuators"): + read_group_parameter(plain, "plain", "controller", "kp") + + +def test_overlapping_groups_are_rejected(): + with pytest.raises( + ValueError, + match="Joint 'joint_1' is assigned to multiple actuator groups: 'first' and 'second'", + ): + ActuatorCollection( + { + "first": _ideal_cfg(["joint_0", "joint_1"], stiffness=1.0, damping=1.0, effort_limit=10.0), + "second": _ideal_cfg(["joint_1", "joint_2"], stiffness=2.0, damping=2.0, effort_limit=20.0), + }, + FakeActuatorControl(), + ) + + +def test_collection_is_mapping_like_and_read_only(): + assert hasattr(actuator_api, "ActuatorTargetCommand") + assert hasattr(actuator_api, "ActuatorOutputCommand") + + control = FakeActuatorControl() + collection = ActuatorCollection({"all": _implicit_cfg()}, control) + assert isinstance(collection.target_command, actuator_api.ActuatorTargetCommand) + assert isinstance(collection.output_command, actuator_api.ActuatorOutputCommand) + + assert list(collection.keys()) == ["all"] + assert collection["all"] is next(iter(collection.values())) + assert list(collection.items())[0][0] == "all" + with pytest.raises(TypeError, match="membership is fixed"): + collection["new"] = collection["all"] + with pytest.raises(TypeError): + del collection["all"] + + assert tuple(collection) == ("all",) + + +def test_custom_singleton_compute_receives_original_selector(): + cfg = _implicit_cfg() + cfg.class_type = SelectorRecordingActuator + collection = ActuatorCollection({"all": cfg}, FakeActuatorControl()) + + collection.compute() + + assert collection["all"].observed_joint_indices == slice(None) + + +def test_multi_group_explicit_outputs_match_pd_formula(): + joint_names = [f"joint_{index}" for index in range(4)] + control = FakeActuatorControl(joint_names=joint_names) + collection = ActuatorCollection( + { + "hips": _ideal_cfg(["joint_0", "joint_2"], stiffness=12.0, damping=1.5, effort_limit=18.0), + "knees": _ideal_cfg(["joint_1", "joint_3"], stiffness=27.0, damping=2.25, effort_limit=31.0), + }, + control, + ) + _assign_deterministic_inputs(collection, control) + + collection.compute() + + stiffness = torch.tensor([[12.0, 27.0, 12.0, 27.0]]) + damping = torch.tensor([[1.5, 2.25, 1.5, 2.25]]) + limit = torch.tensor([[18.0, 31.0, 18.0, 31.0]]) + expected_computed = ( + stiffness * (collection.target_command.position.torch - control.joint_pos.torch) + + damping * (collection.target_command.velocity.torch - control.joint_vel.torch) + + collection.target_command.effort.torch + ) + expected_applied = expected_computed.clamp(-limit, limit) + torch.testing.assert_close(collection.computed_effort.torch, expected_computed, rtol=0.0, atol=0.0) + torch.testing.assert_close(collection.applied_effort.torch, expected_applied, rtol=0.0, atol=0.0) + torch.testing.assert_close(collection["hips"].computed_effort, expected_computed[:, [0, 2]], rtol=0.0, atol=0.0) + torch.testing.assert_close(collection["knees"].applied_effort, expected_applied[:, [1, 3]], rtol=0.0, atol=0.0) + + +def test_disjoint_implicit_groups_share_one_execution_batch(): + assert "is_implicit_model" in ImplicitActuator.__dict__.get("__annotations__", {}) + assert ImplicitActuator.__dict__["is_implicit_model"] is True + + control = FakeActuatorControl(num_envs=1, joint_names=["joint_0", "joint_1", "joint_2", "joint_3"]) + collection = ActuatorCollection( + { + "first": ImplicitActuatorCfg( + joint_names_expr=["joint_0", "joint_2"], stiffness=1.0, damping=1.0, joint_velocity_limit=5.0 + ), + "second": ImplicitActuatorCfg( + joint_names_expr=["joint_1", "joint_3"], stiffness=2.0, damping=2.0, joint_velocity_limit=6.0 + ), + }, + control, + ) + + assert collection._execution_actuators == [] + executor = collection._implicit_executor + assert executor is not None + assert type(executor.actuator) is ImplicitActuator + assert executor.actuator is not collection["first"] + assert executor.group_names == ("first", "second") + + group = collection["first"] + velocity_limit_snapshot = group.actuator_velocity_limit.clone() + control._current_joint_properties["joint_velocity_limit"][:, [0, 2]] = 99.0 + control.joint_stiffness.torch[:, [0, 2]] = torch.tensor([[11.0, 13.0]]) + control.joint_damping.torch[:, [0, 2]] = torch.tensor([[2.0, 3.0]]) + control.joint_effort_limits.torch[:, [0, 2]] = torch.tensor([[7.0, 9.0]]) + collection.target_command.position.torch[:, [0, 2]] = torch.tensor([[1.0, 2.0]]) + collection.target_command.velocity.torch[:, [0, 2]] = torch.tensor([[2.0, 3.0]]) + + collection.compute() + + expected = torch.tensor([[15.0, 35.0]]) + limit = torch.tensor([[7.0, 9.0]]) + assert torch.equal(group.computed_effort, expected) + assert torch.equal(group.applied_effort, expected.clamp(-limit, limit)) + assert torch.equal(group.stiffness, torch.tensor([[11.0, 13.0]])) + assert torch.equal(group.damping, torch.tensor([[2.0, 3.0]])) + assert torch.equal(group.joint_effort_limit, torch.tensor([[7.0, 9.0]])) + assert torch.equal(group.actuator_velocity_limit, velocity_limit_snapshot) + stiffness_before = group.stiffness.clone() + with pytest.warns( + UserWarning, + match=r"ImplicitActuator.stiffness.*write_joint_stiffness_to_sim_index.*randomize_actuator_gains", + ): + group.stiffness = torch.zeros_like(group.stiffness) + assert torch.equal(group.stiffness, stiffness_before) + + +def test_lab_executed_explicit_groups_warn_once(): + explicit_cfgs = { + "ideal": _ideal_cfg(["joint_0"], stiffness=1.0, damping=1.0, effort_limit=10.0), + "delayed": DelayedPDActuatorCfg( + joint_names_expr=["joint_1", "joint_2"], + stiffness=1.0, + damping=1.0, + actuator_effort_limit=10.0, + actuator_velocity_limit=10.0, + max_delay=0, + ), + } + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") + ActuatorCollection(explicit_cfgs, FakeActuatorControl()) + + deprecations = [warning for warning in caught_warnings if warning.category is DeprecationWarning] + assert len(deprecations) == 1 + assert "execution of explicit actuator models is deprecated" in str(deprecations[0].message) + + +@pytest.mark.skipif(not wp.is_cuda_available(), reason="CUDA is unavailable") +@pytest.mark.parametrize( + "actuator_cfg", + [ + ImplicitActuatorCfg(joint_names_expr=["joint_0"], stiffness=2.0, damping=0.0), + IdealPDActuatorCfg( + joint_names_expr=["joint_0"], + stiffness=2.0, + damping=0.0, + actuator_effort_limit=100.0, + actuator_velocity_limit=10.0, + ), + ], + ids=["implicit", "explicit"], +) +def test_actuator_batch_rebinds_cuda_state_provider_on_request( + actuator_cfg: ImplicitActuatorCfg | IdealPDActuatorCfg, +): + control = FakeActuatorControl(num_envs=1, joint_names=["joint_0"], device="cuda:0") + collection = ActuatorCollection({"all": actuator_cfg}, control) + collection.target_command.position.torch.fill_(3.0) + + collection.compute() + control._joint_pos = ProxyArray(wp.full((1, 1), 2.0, dtype=wp.float32, device=control.device)) + if isinstance(actuator_cfg, ImplicitActuatorCfg): + collection.target_command.velocity.torch.fill_(4.0) + collection.target_command.effort.torch.fill_(5.0) + control._joint_vel = ProxyArray(wp.full((1, 1), 1.0, dtype=wp.float32, device=control.device)) + control._joint_stiffness = ProxyArray(wp.full((1, 1), 7.0, dtype=wp.float32, device=control.device)) + control._joint_damping = ProxyArray(wp.full((1, 1), 11.0, dtype=wp.float32, device=control.device)) + control._joint_effort_limits = ProxyArray(wp.full((1, 1), 13.0, dtype=wp.float32, device=control.device)) + collection._rebind_state_inputs() + + collection.compute() + + expected_computed = 45.0 if isinstance(actuator_cfg, ImplicitActuatorCfg) else 2.0 + expected_applied = 13.0 if isinstance(actuator_cfg, ImplicitActuatorCfg) else 2.0 + torch.testing.assert_close( + collection.computed_effort.torch, + torch.tensor([[expected_computed]], device=control.device), + rtol=0.0, + atol=0.0, + ) + torch.testing.assert_close( + collection.applied_effort.torch, + torch.tensor([[expected_applied]], device=control.device), + rtol=0.0, + atol=0.0, + ) + + +def test_partial_coverage_explicit_group_reads_fresh_commands_each_compute(): + control = FakeActuatorControl(joint_names=[f"joint_{index}" for index in range(4)]) + collection = ActuatorCollection( + {"hips": _ideal_cfg(["joint_0", "joint_2"], stiffness=10.0, damping=0.0, effort_limit=1000.0)}, + control, + ) + collection.target_command.position.torch[:, [0, 2]] = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) + + collection.compute() + + expected_first = torch.tensor([[10.0, 20.0], [30.0, 40.0]]) + torch.testing.assert_close(collection.computed_effort.torch[:, [0, 2]].cpu(), expected_first, rtol=0.0, atol=0.0) + + collection.target_command.position.torch.mul_(2.0) + collection.compute() + + torch.testing.assert_close( + collection.computed_effort.torch[:, [0, 2]].cpu(), expected_first * 2.0, rtol=0.0, atol=0.0 + ) + + +def test_native_execution_bypasses_lab_aggregation(monkeypatch): + control = NativeFakeActuatorControl(joint_names=[f"joint_{index}" for index in range(4)]) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") + collection = ActuatorCollection( + { + "hips": _dc_cfg( + ["joint_0", "joint_1"], + stiffness=20.0, + damping=1.0, + effort_limit=40.0, + velocity_limit=10.0, + saturation_effort=60.0, + ), + "knees": _dc_cfg( + ["joint_2", "joint_3"], + stiffness=30.0, + damping=2.0, + effort_limit=70.0, + velocity_limit=20.0, + saturation_effort=120.0, + ), + }, + control, + ) + + assert not [warning for warning in caught_warnings if warning.category is DeprecationWarning] + + assert collection._implicit_executor is None + assert collection._execution_actuators == [] + + def fail_compute(*args, **kwargs): + raise AssertionError("Lab actuator execution must be bypassed") + + monkeypatch.setattr(DCMotor, "compute", fail_compute) + collection.compute() + + +def test_collection_accepts_cached_proxy_joint_indices(): + control = ProxyFinderActuatorControl() + collection = ActuatorCollection({"outer": _implicit_cfg()}, control) + + torch.testing.assert_close(collection["outer"].joint_indices, torch.tensor([0, 2], dtype=torch.int32)) + + +@pytest.mark.parametrize("command_name", ["position", "velocity", "effort"]) +def test_write_command_index_supports_selectors_and_submission(command_name): + control = FakeActuatorControl() + collection = ActuatorCollection({"all": _implicit_cfg()}, control) + setter = getattr(collection.target_command, f"set_{command_name}_index") + command_buffer = getattr(collection.target_command, command_name) + value = torch.tensor([[1.0, 2.0]], dtype=torch.float32) + + setter(value=value, env_ids=[1], joint_ids=[0, 2]) + + expected = torch.zeros(2, 3) + expected[1, 0] = 1.0 + expected[1, 2] = 2.0 + torch.testing.assert_close(command_buffer.torch.cpu(), expected) + assert control.staged_commands == [command_name] + + command_buffer.torch.zero_() + value = torch.tensor([[3.0, 4.0]], dtype=torch.float32) + env_ids = torch.tensor([1], dtype=torch.int64) + joint_ids = wp.array([0, 2], dtype=wp.int64, device="cpu") + + setter(value=value, env_ids=env_ids, joint_ids=joint_ids) + + expected = torch.zeros(2, 3) + expected[1, 0] = 3.0 + expected[1, 2] = 4.0 + torch.testing.assert_close(command_buffer.torch.cpu(), expected) + + collection.compute() + collection.submit_commands() + + torch.testing.assert_close(getattr(collection.output_command, command_name).torch.cpu(), expected) + assert control.submitted + + +@pytest.mark.parametrize("command_name", ["position", "velocity", "effort"]) +def test_write_command_mask_uses_full_sized_value(command_name): + control = FakeActuatorControl() + collection = ActuatorCollection({"all": _implicit_cfg()}, control) + value = torch.arange(6, dtype=torch.float32).reshape(2, 3) + env_mask = wp.array([True, False], dtype=wp.bool, device="cpu") + joint_mask = wp.array([False, True, True], dtype=wp.bool, device="cpu") + + getattr(collection.target_command, f"set_{command_name}_mask")( + value=value, env_mask=env_mask, joint_mask=joint_mask + ) + + expected = torch.zeros(2, 3) + expected[0, 1:] = value[0, 1:] + torch.testing.assert_close(getattr(collection.target_command, command_name).torch.cpu(), expected) + assert control.staged_commands == [command_name] + + with pytest.raises(TypeError, match="wp.bool"): + getattr(collection.target_command, f"set_{command_name}_mask")( + value=value, env_mask=wp.array([1, 0], dtype=wp.int32, device="cpu"), joint_mask=joint_mask + ) diff --git a/source/isaaclab/test/actuators/test_dc_motor.py b/source/isaaclab/test/actuators/test_dc_motor.py index a3efc49b3ca5..a1cc2a620d41 100644 --- a/source/isaaclab/test/actuators/test_dc_motor.py +++ b/source/isaaclab/test/actuators/test_dc_motor.py @@ -27,9 +27,9 @@ def test_dc_motor_init_minimum(num_envs, num_joints, device): joint_names_expr=joint_names, stiffness=stiffness, damping=damping, - effort_limit=effort_limit, + actuator_effort_limit=effort_limit, saturation_effort=saturation_effort, - velocity_limit=velocity_limit, + actuator_velocity_limit=velocity_limit, ) # assume Articulation class: # - finds joints (names and ids) associate with the provided joint_names_expr @@ -46,11 +46,11 @@ def test_dc_motor_init_minimum(num_envs, num_joints, device): torch.testing.assert_close(actuator.computed_effort, torch.zeros(num_envs, num_joints, device=device)) torch.testing.assert_close(actuator.applied_effort, torch.zeros(num_envs, num_joints, device=device)) torch.testing.assert_close( - actuator.effort_limit, + actuator.actuator_effort_limit, effort_limit * torch.ones(num_envs, num_joints, device=device), ) torch.testing.assert_close( - actuator.velocity_limit, velocity_limit * torch.ones(num_envs, num_joints, device=device) + actuator.actuator_velocity_limit, velocity_limit * torch.ones(num_envs, num_joints, device=device) ) @@ -74,7 +74,7 @@ def test_dc_motor_clip(num_envs, num_joints, device, test_point): 9 - less than effort limit but outside torque speed curve and inside corner velocity (quadrant 4) e - effort_limit s - saturation_effort - v - velocity_limit + v - actuator_velocity_limit c - corner velocity \ - torque-speed linear boundary between v and s each torque_speed_point will be tested in quadrant 3 and 4 @@ -158,8 +158,8 @@ def test_dc_motor_clip(num_envs, num_joints, device, test_point): joint_names_expr=joint_names, stiffness=stiffness, damping=damping, - effort_limit=effort_lim, - velocity_limit=velocity_limit, + actuator_effort_limit=effort_lim, + actuator_velocity_limit=velocity_limit, saturation_effort=saturation_effort, ) diff --git a/source/isaaclab/test/actuators/test_ideal_pd_actuator.py b/source/isaaclab/test/actuators/test_ideal_pd_actuator.py index 01f19007ae94..e82bc2849e80 100644 --- a/source/isaaclab/test/actuators/test_ideal_pd_actuator.py +++ b/source/isaaclab/test/actuators/test_ideal_pd_actuator.py @@ -23,15 +23,11 @@ def test_ideal_pd_actuator_init_minimum(num_envs, num_joints, device, usd_defaul joint_ids = [d for d in range(num_joints)] stiffness = None if usd_default else 200 damping = None if usd_default else 10 - friction = None if usd_default else 0.1 - armature = None if usd_default else 0.2 actuator_cfg = IdealPDActuatorCfg( joint_names_expr=joint_names, stiffness=stiffness, damping=damping, - armature=armature, - friction=friction, ) # assume Articulation class: # - finds joints (names and ids) associate with the provided joint_names_expr @@ -39,8 +35,6 @@ def test_ideal_pd_actuator_init_minimum(num_envs, num_joints, device, usd_defaul # faux usd defaults stiffness_default = 300 damping_default = 20 - friction_default = 0.0 - armature_default = 0.0 actuator = actuator_cfg.class_type( actuator_cfg, @@ -50,8 +44,6 @@ def test_ideal_pd_actuator_init_minimum(num_envs, num_joints, device, usd_defaul device=device, stiffness=stiffness_default, damping=damping_default, - friction=friction_default, - armature=armature_default, ) # check initialized actuator @@ -60,99 +52,41 @@ def test_ideal_pd_actuator_init_minimum(num_envs, num_joints, device, usd_defaul torch.testing.assert_close(actuator.computed_effort, torch.zeros(num_envs, num_joints, device=device)) torch.testing.assert_close(actuator.applied_effort, torch.zeros(num_envs, num_joints, device=device)) - torch.testing.assert_close(actuator.effort_limit, torch.inf * torch.ones(num_envs, num_joints, device=device)) torch.testing.assert_close( - actuator.effort_limit_sim, actuator._DEFAULT_MAX_EFFORT_SIM * torch.ones(num_envs, num_joints, device=device) + actuator.actuator_effort_limit, torch.inf * torch.ones(num_envs, num_joints, device=device) + ) + with pytest.warns(DeprecationWarning, match="actuator_effort_limit"): + torch.testing.assert_close(actuator.effort_limit, actuator.actuator_effort_limit) + torch.testing.assert_close( + actuator.actuator_velocity_limit, torch.inf * torch.ones(num_envs, num_joints, device=device) ) - torch.testing.assert_close(actuator.velocity_limit, torch.inf * torch.ones(num_envs, num_joints, device=device)) - torch.testing.assert_close(actuator.velocity_limit_sim, torch.inf * torch.ones(num_envs, num_joints, device=device)) if not usd_default: torch.testing.assert_close(actuator.stiffness, stiffness * torch.ones(num_envs, num_joints, device=device)) torch.testing.assert_close(actuator.damping, damping * torch.ones(num_envs, num_joints, device=device)) - torch.testing.assert_close(actuator.armature, armature * torch.ones(num_envs, num_joints, device=device)) - torch.testing.assert_close(actuator.friction, friction * torch.ones(num_envs, num_joints, device=device)) else: torch.testing.assert_close( actuator.stiffness, stiffness_default * torch.ones(num_envs, num_joints, device=device) ) torch.testing.assert_close(actuator.damping, damping_default * torch.ones(num_envs, num_joints, device=device)) - torch.testing.assert_close( - actuator.armature, armature_default * torch.ones(num_envs, num_joints, device=device) - ) - torch.testing.assert_close( - actuator.friction, friction_default * torch.ones(num_envs, num_joints, device=device) - ) - - -@pytest.mark.parametrize("num_envs", [1, 2]) -@pytest.mark.parametrize("num_joints", [1, 2]) -@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) -@pytest.mark.parametrize("effort_lim", [None, 300]) -@pytest.mark.parametrize("effort_lim_sim", [None, 400]) -def test_ideal_pd_actuator_init_effort_limits(num_envs, num_joints, device, effort_lim, effort_lim_sim): - """Test initialization of ideal pd actuator with effort limits.""" - # used as a standin for the usd default value read in by articulation. - # This value should not be propagated for ideal pd actuators - effort_lim_default = 5000 - - joint_names = [f"joint_{d}" for d in range(num_joints)] - joint_ids = [d for d in range(num_joints)] - - actuator_cfg = IdealPDActuatorCfg( - joint_names_expr=joint_names, - stiffness=200, - damping=10, - effort_limit=effort_lim, - effort_limit_sim=effort_lim_sim, - ) - - actuator = actuator_cfg.class_type( - actuator_cfg, - joint_names=joint_names, - joint_ids=joint_ids, - num_envs=num_envs, - device=device, - stiffness=actuator_cfg.stiffness, - damping=actuator_cfg.damping, - effort_limit=effort_lim_default, - ) - - if effort_lim is not None and effort_lim_sim is None: - effort_lim_expected = effort_lim - effort_lim_sim_expected = actuator._DEFAULT_MAX_EFFORT_SIM - - elif effort_lim is None and effort_lim_sim is not None: - effort_lim_expected = effort_lim_default - effort_lim_sim_expected = effort_lim_sim - - elif effort_lim is None and effort_lim_sim is None: - effort_lim_expected = effort_lim_default - effort_lim_sim_expected = actuator._DEFAULT_MAX_EFFORT_SIM - - elif effort_lim is not None and effort_lim_sim is not None: - effort_lim_expected = effort_lim - effort_lim_sim_expected = effort_lim_sim - - torch.testing.assert_close( - actuator.effort_limit, effort_lim_expected * torch.ones(num_envs, num_joints, device=device) - ) - torch.testing.assert_close( - actuator.effort_limit_sim, effort_lim_sim_expected * torch.ones(num_envs, num_joints, device=device) - ) @pytest.mark.parametrize("num_envs", [1, 2]) @pytest.mark.parametrize("num_joints", [1, 2]) @pytest.mark.parametrize("device", ["cuda:0", "cpu"]) -@pytest.mark.parametrize("velocity_lim", [None, 300]) -@pytest.mark.parametrize("velocity_lim_sim", [None, 400]) -def test_ideal_pd_actuator_init_velocity_limits(num_envs, num_joints, device, velocity_lim, velocity_lim_sim): - """Test initialization of ideal pd actuator with velocity limits. +@pytest.mark.parametrize("cfg_limit", [None, 300]) +@pytest.mark.parametrize( + "limit_name", + ["actuator_effort_limit", "actuator_velocity_limit"], +) +def test_ideal_pd_actuator_init_limits(num_envs, num_joints, device, cfg_limit, limit_name): + """Test that a cfg-provided limit wins over the constructor default for effort and velocity limits. Note Ideal PD actuator does not use velocity limits in computation, they are passed to physics via articulations. """ - velocity_limit_default = 1000 + # used as a standin for the usd default value read in by articulation. + limit_default = 5000 + joint_names = [f"joint_{d}" for d in range(num_joints)] joint_ids = [d for d in range(num_joints)] @@ -160,8 +94,7 @@ def test_ideal_pd_actuator_init_velocity_limits(num_envs, num_joints, device, ve joint_names_expr=joint_names, stiffness=200, damping=10, - velocity_limit=velocity_lim, - velocity_limit_sim=velocity_lim_sim, + **{limit_name: cfg_limit}, ) actuator = actuator_cfg.class_type( @@ -172,26 +105,11 @@ def test_ideal_pd_actuator_init_velocity_limits(num_envs, num_joints, device, ve device=device, stiffness=actuator_cfg.stiffness, damping=actuator_cfg.damping, - velocity_limit=velocity_limit_default, - ) - if velocity_lim is not None and velocity_lim_sim is None: - vel_lim_expected = velocity_lim - vel_lim_sim_expected = velocity_limit_default - elif velocity_lim is None and velocity_lim_sim is not None: - vel_lim_expected = velocity_lim_sim - vel_lim_sim_expected = velocity_lim_sim - elif velocity_lim is None and velocity_lim_sim is None: - vel_lim_expected = velocity_limit_default - vel_lim_sim_expected = velocity_limit_default - elif velocity_lim is not None and velocity_lim_sim is not None: - vel_lim_expected = velocity_lim - vel_lim_sim_expected = velocity_lim_sim - - torch.testing.assert_close( - actuator.velocity_limit, vel_lim_expected * torch.ones(num_envs, num_joints, device=device) + **{limit_name: limit_default}, ) + limit_expected = cfg_limit if cfg_limit is not None else limit_default torch.testing.assert_close( - actuator.velocity_limit_sim, vel_lim_sim_expected * torch.ones(num_envs, num_joints, device=device) + getattr(actuator, limit_name), limit_expected * torch.ones(num_envs, num_joints, device=device) ) @@ -210,7 +128,7 @@ def test_ideal_pd_compute(num_envs, num_joints, device, effort_lim): joint_names_expr=joint_names, stiffness=stiffness, damping=damping, - effort_limit=effort_lim, + actuator_effort_limit=effort_lim, ) actuator = actuator_cfg.class_type( diff --git a/source/isaaclab/test/actuators/test_implicit_actuator.py b/source/isaaclab/test/actuators/test_implicit_actuator.py index 39018d694a1e..c5017488cd03 100644 --- a/source/isaaclab/test/actuators/test_implicit_actuator.py +++ b/source/isaaclab/test/actuators/test_implicit_actuator.py @@ -3,53 +3,30 @@ # # SPDX-License-Identifier: BSD-3-Clause -from isaaclab.app import AppLauncher - -HEADLESS = True - -# if not AppLauncher.instance(): -simulation_app = AppLauncher(headless=HEADLESS).app - -"""Rest of imports follows""" - import pytest import torch from isaaclab.actuators import ImplicitActuatorCfg -from isaaclab.sim import build_simulation_context pytestmark = pytest.mark.integration -@pytest.fixture -def sim(request): - """Create simulation context with the specified device.""" - device = request.getfixturevalue("device") - with build_simulation_context(device=device) as sim: - sim._app_control_on_stop_handle = None - yield sim - - @pytest.mark.parametrize("num_envs", [1, 2]) @pytest.mark.parametrize("num_joints", [1, 2]) @pytest.mark.parametrize("device", ["cuda:0", "cpu"]) @pytest.mark.parametrize("usd_default", [False, True]) -def test_implicit_actuator_init_minimum(sim, num_envs, num_joints, device, usd_default): +def test_implicit_actuator_init_minimum(num_envs, num_joints, device, usd_default): """Test initialization of implicit actuator with minimum configuration.""" joint_names = [f"joint_{d}" for d in range(num_joints)] joint_ids = [d for d in range(num_joints)] stiffness = None if usd_default else 200 damping = None if usd_default else 10 - friction = None if usd_default else 0.1 - armature = None if usd_default else 0.2 actuator_cfg = ImplicitActuatorCfg( joint_names_expr=joint_names, stiffness=stiffness, damping=damping, - armature=armature, - friction=friction, ) # assume Articulation class: # - finds joints (names and ids) associate with the provided joint_names_expr @@ -57,8 +34,6 @@ def test_implicit_actuator_init_minimum(sim, num_envs, num_joints, device, usd_d # faux usd defaults stiffness_default = 300 damping_default = 20 - friction_default = 0.0 - armature_default = 0.0 actuator = actuator_cfg.class_type( actuator_cfg, @@ -68,8 +43,6 @@ def test_implicit_actuator_init_minimum(sim, num_envs, num_joints, device, usd_d device=device, stiffness=stiffness_default, damping=damping_default, - friction=friction_default, - armature=armature_default, ) # check initialized actuator @@ -78,37 +51,35 @@ def test_implicit_actuator_init_minimum(sim, num_envs, num_joints, device, usd_d torch.testing.assert_close(actuator.computed_effort, torch.zeros(num_envs, num_joints, device=device)) torch.testing.assert_close(actuator.applied_effort, torch.zeros(num_envs, num_joints, device=device)) - torch.testing.assert_close(actuator.effort_limit, torch.inf * torch.ones(num_envs, num_joints, device=device)) - torch.testing.assert_close(actuator.effort_limit_sim, torch.inf * torch.ones(num_envs, num_joints, device=device)) - torch.testing.assert_close(actuator.velocity_limit, torch.inf * torch.ones(num_envs, num_joints, device=device)) - torch.testing.assert_close(actuator.velocity_limit_sim, torch.inf * torch.ones(num_envs, num_joints, device=device)) + torch.testing.assert_close(actuator.joint_effort_limit, torch.inf * torch.ones(num_envs, num_joints, device=device)) + with pytest.warns(DeprecationWarning, match="actuator_effort_limit"): + torch.testing.assert_close(actuator.effort_limit, actuator.joint_effort_limit) + torch.testing.assert_close( + actuator.actuator_velocity_limit, torch.inf * torch.ones(num_envs, num_joints, device=device) + ) if not usd_default: torch.testing.assert_close(actuator.stiffness, stiffness * torch.ones(num_envs, num_joints, device=device)) torch.testing.assert_close(actuator.damping, damping * torch.ones(num_envs, num_joints, device=device)) - torch.testing.assert_close(actuator.armature, armature * torch.ones(num_envs, num_joints, device=device)) - torch.testing.assert_close(actuator.friction, friction * torch.ones(num_envs, num_joints, device=device)) else: torch.testing.assert_close( actuator.stiffness, stiffness_default * torch.ones(num_envs, num_joints, device=device) ) torch.testing.assert_close(actuator.damping, damping_default * torch.ones(num_envs, num_joints, device=device)) - torch.testing.assert_close( - actuator.armature, armature_default * torch.ones(num_envs, num_joints, device=device) - ) - torch.testing.assert_close( - actuator.friction, friction_default * torch.ones(num_envs, num_joints, device=device) - ) @pytest.mark.parametrize("num_envs", [1, 2]) @pytest.mark.parametrize("num_joints", [1, 2]) @pytest.mark.parametrize("device", ["cuda:0", "cpu"]) -@pytest.mark.parametrize("effort_lim", [None, 300, 200]) -@pytest.mark.parametrize("effort_lim_sim", [None, 400, 200]) -def test_implicit_actuator_init_effort_limits(sim, num_envs, num_joints, device, effort_lim, effort_lim_sim): - """Test independent resolution of the model-facing effort limit and solver clamp.""" - effort_limit_default = 5000 +@pytest.mark.parametrize("cfg_limit", [None, 300]) +@pytest.mark.parametrize( + "limit_name", + ["joint_effort_limit", "actuator_velocity_limit"], +) +def test_implicit_actuator_init_limits(num_envs, num_joints, device, cfg_limit, limit_name): + """Test that a cfg-provided limit wins over the constructor default for effort and velocity limits.""" + # used as a standin for the usd default value read in by articulation. + limit_default = 5000 joint_names = [f"joint_{d}" for d in range(num_joints)] joint_ids = [d for d in range(num_joints)] @@ -117,8 +88,7 @@ def test_implicit_actuator_init_effort_limits(sim, num_envs, num_joints, device, joint_names_expr=joint_names, stiffness=200, damping=10, - effort_limit=effort_lim, - effort_limit_sim=effort_lim_sim, + **{limit_name: cfg_limit}, ) actuator = actuator_cfg.class_type( @@ -129,76 +99,85 @@ def test_implicit_actuator_init_effort_limits(sim, num_envs, num_joints, device, device=device, stiffness=actuator_cfg.stiffness, damping=actuator_cfg.damping, - effort_limit=effort_limit_default, - ) - effort_lim_sim_expected = effort_lim_sim - if effort_lim_sim_expected is None: - effort_lim_sim_expected = effort_lim if effort_lim is not None else effort_limit_default - if effort_lim is None: - assert actuator.cfg.effort_limit == actuator.cfg.effort_limit_sim - effort_lim_expected = effort_lim_sim_expected - else: - assert actuator.cfg.effort_limit == effort_lim - effort_lim_expected = effort_lim - - torch.testing.assert_close( - actuator.effort_limit, effort_lim_expected * torch.ones(num_envs, num_joints, device=device) + **{limit_name: limit_default}, ) + limit_expected = cfg_limit if cfg_limit is not None else limit_default torch.testing.assert_close( - actuator.effort_limit_sim, effort_lim_sim_expected * torch.ones(num_envs, num_joints, device=device) + getattr(actuator, limit_name), limit_expected * torch.ones(num_envs, num_joints, device=device) ) -@pytest.mark.parametrize("num_envs", [1, 2]) -@pytest.mark.parametrize("num_joints", [1, 2]) @pytest.mark.parametrize("device", ["cuda:0", "cpu"]) -@pytest.mark.parametrize("velocity_lim", [None, 300, 200]) -@pytest.mark.parametrize("velocity_lim_sim", [None, 400, 200]) -def test_implicit_actuator_init_velocity_limits(sim, num_envs, num_joints, device, velocity_lim, velocity_lim_sim): - """Test initialization of implicit actuator with velocity limits. - - The joint velocity limit ``velocity_limit`` and the solver clamp ``velocity_limit_sim`` are resolved - independently: the joint velocity limit is never pushed to the solver, and setting both is valid. - When only the solver clamp is set, it doubles as the joint velocity limit. - """ - velocity_limit_default = 1000 - joint_names = [f"joint_{d}" for d in range(num_joints)] - joint_ids = [d for d in range(num_joints)] - +def test_implicit_actuator_separate_rated_and_solver_effort_limits(device): + """A configured rated limit stays on the actuator while the solver keeps its own clamp.""" + joint_names = ["joint_0"] actuator_cfg = ImplicitActuatorCfg( joint_names_expr=joint_names, stiffness=200, damping=10, - velocity_limit=velocity_lim, - velocity_limit_sim=velocity_lim_sim, + actuator_effort_limit=87.0, + joint_effort_limit=870.0, ) - actuator = actuator_cfg.class_type( actuator_cfg, joint_names=joint_names, - joint_ids=joint_ids, - num_envs=num_envs, + joint_ids=[0], + num_envs=2, device=device, stiffness=actuator_cfg.stiffness, damping=actuator_cfg.damping, - velocity_limit=velocity_limit_default, ) - vel_lim_sim_expected = velocity_lim_sim if velocity_lim_sim is not None else velocity_limit_default - if velocity_lim is None: - # the joint velocity limit falls back to the solver clamp - assert actuator.cfg.velocity_limit == actuator.cfg.velocity_limit_sim - vel_lim_expected = vel_lim_sim_expected - else: - # the configured joint velocity limit is kept and not overwritten by the solver clamp - assert actuator.cfg.velocity_limit == velocity_lim - vel_lim_expected = velocity_lim + torch.testing.assert_close(actuator.actuator_effort_limit, 87.0 * torch.ones(2, 1, device=device)) + torch.testing.assert_close(actuator.joint_effort_limit, 870.0 * torch.ones(2, 1, device=device)) - torch.testing.assert_close( - actuator.velocity_limit, vel_lim_expected * torch.ones(num_envs, num_joints, device=device) + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +def test_implicit_actuator_deprecated_effort_aliases_resolve_rated_and_solver(device): + """Deprecated ``effort_limit``/``effort_limit_sim`` map to the rated and solver limits.""" + joint_names = ["joint_0"] + actuator_cfg = ImplicitActuatorCfg( + joint_names_expr=joint_names, + stiffness=200, + damping=10, + effort_limit=87.0, + effort_limit_sim=870.0, ) - torch.testing.assert_close( - actuator.velocity_limit_sim, vel_lim_sim_expected * torch.ones(num_envs, num_joints, device=device) + with pytest.warns(DeprecationWarning): + actuator = actuator_cfg.class_type( + actuator_cfg, + joint_names=joint_names, + joint_ids=[0], + num_envs=2, + device=device, + stiffness=actuator_cfg.stiffness, + damping=actuator_cfg.damping, + ) + torch.testing.assert_close(actuator.actuator_effort_limit, 87.0 * torch.ones(2, 1, device=device)) + torch.testing.assert_close(actuator.joint_effort_limit, 870.0 * torch.ones(2, 1, device=device)) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +def test_implicit_actuator_deprecated_effort_limit_alone_reaches_solver(device): + """Without a separate solver clamp, the deprecated rated limit also reaches the solver.""" + joint_names = ["joint_0"] + actuator_cfg = ImplicitActuatorCfg( + joint_names_expr=joint_names, + stiffness=200, + damping=10, + effort_limit=87.0, ) + with pytest.warns(DeprecationWarning): + actuator = actuator_cfg.class_type( + actuator_cfg, + joint_names=joint_names, + joint_ids=[0], + num_envs=2, + device=device, + stiffness=actuator_cfg.stiffness, + damping=actuator_cfg.damping, + ) + torch.testing.assert_close(actuator.actuator_effort_limit, 87.0 * torch.ones(2, 1, device=device)) + torch.testing.assert_close(actuator.joint_effort_limit, 87.0 * torch.ones(2, 1, device=device)) if __name__ == "__main__": diff --git a/source/isaaclab/test/actuators/test_newton_adapter.py b/source/isaaclab/test/actuators/test_newton_adapter.py new file mode 100644 index 000000000000..6b20b396cbee --- /dev/null +++ b/source/isaaclab/test/actuators/test_newton_adapter.py @@ -0,0 +1,246 @@ +# 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 + +"""Tests for the PhysX-side Newton actuator adapter.""" + +import numpy as np +import pytest +from newton.actuators import ClampingDCMotor, ClampingMaxEffort, ClampingPositionBased, ControllerPD + +from pxr import Usd, UsdGeom, UsdPhysics + +from isaaclab.actuators import ActuatorBaseCfg, DCMotor, DCMotorCfg, DelayedPDActuatorCfg, RemotizedPDActuatorCfg +from isaaclab.actuators.newton import NewtonActuatorAdapter +from isaaclab.sim.schemas.schemas_actuators import _author_actuator_prims +from isaaclab.utils.configclass import configclass + +_JOINT_NAMES = ["pd_a", "pd_b", "dc_a", "dc_b", "remote_a", "remote_b"] + + +@configclass +class UnsupportedNewtonActuatorCfg(ActuatorBaseCfg): + """Explicit actuator config intentionally unsupported by Newton authoring.""" + + class_type: str = "unsupported:ExplicitActuator" + + +@configclass +class CustomDCMotorCfg(DCMotorCfg): + """DC motor config that selects a custom Lab actuator implementation.""" + + class_type: str = "unsupported:CustomDCMotor" + + +class MisleadingImplicitActuatorDCMotor(DCMotor): + """Explicit actuator whose class name contains ``ImplicitActuator``.""" + + +def _make_actuator_stage() -> Usd.Stage: + stage = Usd.Stage.CreateInMemory() + UsdGeom.Xform.Define(stage, "/World/Robot") + + bodies = [UsdGeom.Xform.Define(stage, f"/World/Robot/body_{index}") for index in range(len(_JOINT_NAMES))] + for body in bodies: + UsdPhysics.RigidBodyAPI.Apply(body.GetPrim()) + + joints = [UsdPhysics.RevoluteJoint.Define(stage, f"/World/Robot/{name}") for name in _JOINT_NAMES] + for joint, body in zip(joints, bodies, strict=True): + joint.CreateBody1Rel().SetTargets([body.GetPath()]) + + _author_actuator_prims( + stage, + "/World/Robot", + { + "pd_a": DelayedPDActuatorCfg( + joint_names_expr=["pd_a"], stiffness=11.0, damping=1.5, actuator_effort_limit=21.0, max_delay=2 + ), + "pd_b": DelayedPDActuatorCfg( + joint_names_expr=["pd_b"], stiffness=22.0, damping=2.5, actuator_effort_limit=32.0, max_delay=4 + ), + "dc_a": DCMotorCfg( + joint_names_expr=["dc_a"], + stiffness=33.0, + damping=3.5, + actuator_effort_limit=43.0, + actuator_velocity_limit=7.0, + saturation_effort=53.0, + ), + "dc_b": DCMotorCfg( + joint_names_expr=["dc_b"], + stiffness=44.0, + damping=4.5, + actuator_effort_limit=54.0, + actuator_velocity_limit=8.0, + saturation_effort=64.0, + ), + "remote_a": RemotizedPDActuatorCfg( + joint_names_expr=["remote_a"], + stiffness=55.0, + damping=5.5, + actuator_effort_limit=65.0, + max_delay=1, + joint_parameter_lookup=[[-1.0, 1.0, 10.0], [1.0, 1.0, 20.0]], + ), + "remote_b": RemotizedPDActuatorCfg( + joint_names_expr=["remote_b"], + stiffness=55.0, + damping=5.5, + actuator_effort_limit=65.0, + max_delay=1, + joint_parameter_lookup=[[-1.0, 1.0, 11.0], [1.0, 1.0, 21.0]], + ), + }, + ) + return stage + + +def test_from_usd_groups_by_structure_and_preserves_per_dof_values(): + """Aggregate scalar variants while keeping incompatible shared lookup tables separate.""" + actuators = NewtonActuatorAdapter.from_usd( + stage=_make_actuator_stage(), + joint_names=_JOINT_NAMES, + num_envs=2, + num_joints=len(_JOINT_NAMES), + device="cpu", + articulation_prim_path="/World/Robot", + ).actuators + + assert len(actuators) == 4 + + pd = next(actuator for actuator in actuators if [type(c) for c in actuator.clamping] == [ClampingMaxEffort]) + assert type(pd.controller) is ControllerPD + np.testing.assert_array_equal(pd.indices.numpy(), [0, 1, 6, 7]) + np.testing.assert_allclose(pd.controller.kp.numpy(), [11.0, 22.0, 11.0, 22.0]) + np.testing.assert_allclose(pd.controller.kd.numpy(), [1.5, 2.5, 1.5, 2.5]) + np.testing.assert_allclose(pd.clamping[0].max_effort.numpy(), [21.0, 32.0, 21.0, 32.0]) + np.testing.assert_array_equal(pd.delay.delay_steps.numpy(), [2, 4, 2, 4]) + assert pd.delay.buf_depth == 4 + + dc = next(actuator for actuator in actuators if [type(c) for c in actuator.clamping] == [ClampingDCMotor]) + assert type(dc.controller) is ControllerPD + assert dc.delay is None + np.testing.assert_array_equal(dc.indices.numpy(), [2, 3, 8, 9]) + np.testing.assert_allclose(dc.controller.kp.numpy(), [33.0, 44.0, 33.0, 44.0]) + np.testing.assert_allclose(dc.controller.kd.numpy(), [3.5, 4.5, 3.5, 4.5]) + np.testing.assert_allclose(dc.clamping[0].saturation_effort.numpy(), [53.0, 64.0, 53.0, 64.0]) + np.testing.assert_allclose(dc.clamping[0].velocity_limit.numpy(), [7.0, 8.0, 7.0, 8.0]) + np.testing.assert_allclose(dc.clamping[0].max_motor_effort.numpy(), [43.0, 54.0, 43.0, 54.0]) + + remotized = [ + actuator + for actuator in actuators + if any(type(clamping) is ClampingPositionBased for clamping in actuator.clamping) + ] + assert len(remotized) == 2 + assert {tuple(actuator.indices.numpy()) for actuator in remotized} == {(4, 10), (5, 11)} + assert all(not any(type(clamping) is ClampingMaxEffort for clamping in actuator.clamping) for actuator in remotized) + assert { + tuple(next(c for c in actuator.clamping if type(c) is ClampingPositionBased).lookup_efforts.numpy()) + for actuator in remotized + } == {(10.0, 20.0), (11.0, 21.0)} + + +@pytest.mark.parametrize( + ("configured_limit", "expected_limits"), + [(None, (71.0, 72.0)), ({"pd_a": 5.0}, (5.0, 0.0))], +) +def test_schema_authoring_matches_lab_effort_limit_resolution(configured_limit, expected_limits): + """Match authored fallback and partial-map resolution on native and Lab paths.""" + stage = _make_actuator_stage() + for joint_name, authored_limit in zip(("pd_a", "pd_b"), (71.0, 72.0), strict=True): + joint_prim = stage.GetPrimAtPath(f"/World/Robot/{joint_name}") + UsdPhysics.DriveAPI.Apply(joint_prim, "angular").CreateMaxForceAttr(authored_limit) + cfg = DelayedPDActuatorCfg( + joint_names_expr=["pd_.*"], + stiffness=1.0, + damping=0.0, + actuator_effort_limit=configured_limit, + max_delay=0, + ) + + _author_actuator_prims(stage, "/World/Robot", {"fallback": cfg}) + + for joint_name, expected_limit in zip(("pd_a", "pd_b"), expected_limits, strict=True): + actuator_prim = stage.GetPrimAtPath(f"/World/Robot/fallback_{joint_name}_actuator") + assert actuator_prim.GetAttribute("newton:maxEffort").Get() == pytest.approx(expected_limit) + + +@pytest.mark.parametrize( + "configured_limit", + [ + {"pd_.*": 5.0, "pd_a": 7.0}, + {"missing_joint": 5.0}, + ], +) +def test_schema_authoring_rejects_invalid_effort_limit_patterns(configured_limit): + stage = _make_actuator_stage() + cfg = DelayedPDActuatorCfg( + joint_names_expr=["pd_.*"], + stiffness=1.0, + damping=0.0, + actuator_effort_limit=configured_limit, + max_delay=0, + ) + + with pytest.raises(ValueError): + _author_actuator_prims(stage, "/World/Robot", {"invalid_limits": cfg}) + + +@pytest.mark.parametrize( + ("group_name", "cfg"), + [ + ("unsupported", UnsupportedNewtonActuatorCfg(joint_names_expr=["pd_a"], stiffness=0.0, damping=0.0)), + ( + "custom_dc", + CustomDCMotorCfg( + joint_names_expr=["pd_a"], + stiffness=0.0, + damping=0.0, + actuator_effort_limit=1.0, + actuator_velocity_limit=1.0, + saturation_effort=1.0, + ), + ), + ( + "misleading_name", + CustomDCMotorCfg( + class_type=f"{__name__}:MisleadingImplicitActuatorDCMotor", + joint_names_expr=["pd_a"], + stiffness=0.0, + damping=0.0, + actuator_effort_limit=1.0, + actuator_velocity_limit=1.0, + saturation_effort=1.0, + ), + ), + ], +) +def test_schema_authoring_rejects_unsupported_explicit_cfg_before_removal(group_name, cfg): + """Leave existing actuator prims intact when native authoring rejects a config.""" + stage = _make_actuator_stage() + actuator_path = "/World/Robot/pd_a_pd_a_actuator" + + with pytest.raises(ValueError, match=rf"{group_name}.*{type(cfg).__name__}.*use_newton_actuators"): + _author_actuator_prims(stage, "/World/Robot", {group_name: cfg}) + + assert stage.GetPrimAtPath(actuator_path).IsValid() + + +def test_schema_authoring_accepts_supported_public_actuator_alias(): + """Accept a public import path that resolves to a supported actuator class.""" + stage = _make_actuator_stage() + cfg = DCMotorCfg( + class_type="isaaclab.actuators:DCMotor", + joint_names_expr=["pd_a"], + stiffness=1.0, + damping=0.1, + actuator_effort_limit=2.0, + actuator_velocity_limit=3.0, + saturation_effort=4.0, + ) + + _author_actuator_prims(stage, "/World/Robot", {"public_alias": cfg}) + + assert stage.GetPrimAtPath("/World/Robot/public_alias_pd_a_actuator").IsValid() diff --git a/source/isaaclab/test/assets/_articulation_iface_test_utils.py b/source/isaaclab/test/assets/_articulation_iface_test_utils.py index a3b8887d70dd..0b2caf776390 100644 --- a/source/isaaclab/test/assets/_articulation_iface_test_utils.py +++ b/source/isaaclab/test/assets/_articulation_iface_test_utils.py @@ -13,6 +13,7 @@ from _iface_test_boot import simulation_app import numpy as np +import torch import warp as wp from isaaclab.assets.articulation.articulation_cfg import ArticulationCfg @@ -83,6 +84,7 @@ def create_physx_articulation( joint_ordering=joint_ordering, body_ordering=body_ordering, ) + object.__setattr__(articulation, "_sim_cfg", None) # Create PhysX mock view mock_view = PhysXMockArticulationViewWarp( @@ -134,8 +136,6 @@ def create_physx_articulation( object.__setattr__(articulation, "_debug_vis_handle", None) # Set up other required attributes - object.__setattr__(articulation, "actuators", {}) - object.__setattr__(articulation, "_has_implicit_actuators", False) object.__setattr__(articulation, "_ALL_INDICES", wp.array(np.arange(num_instances, dtype=np.int32), device=device)) object.__setattr__( articulation, "_ALL_BODY_INDICES", wp.array(np.arange(num_bodies, dtype=np.int32), device=device) @@ -173,18 +173,6 @@ def create_physx_articulation( articulation._resolve_and_install_ordering_maps() articulation._ordering_configure_backend_staging() - # Initialize joint targets - joint_target_shape = (num_instances, num_joints) - object.__setattr__( - articulation, "_joint_pos_target_sim", wp.zeros(joint_target_shape, dtype=wp.float32, device=device) - ) - object.__setattr__( - articulation, "_joint_vel_target_sim", wp.zeros(joint_target_shape, dtype=wp.float32, device=device) - ) - object.__setattr__( - articulation, "_joint_effort_target_sim", wp.zeros(joint_target_shape, dtype=wp.float32, device=device) - ) - # Cached .view(wp.float32) wrappers object.__setattr__(articulation, "_root_link_pose_w_f32", None) object.__setattr__(articulation, "_root_com_vel_w_f32", None) @@ -213,6 +201,8 @@ def create_physx_articulation( object.__setattr__(articulation, "_cpu_body_coms", wp.zeros((N, B, 7), dtype=wp.float32, device="cpu")) object.__setattr__(articulation, "_cpu_body_inertia", wp.zeros((N, B, 9), dtype=wp.float32, device="cpu")) + articulation._process_actuators_cfg() + return articulation, mock_view @@ -240,6 +230,7 @@ def create_ovphysx_articulation( joint_ordering=joint_ordering, body_ordering=body_ordering, ) + object.__setattr__(articulation, "_sim_cfg", None) # Create mock binding set mock_bindings = MockOvPhysxBindingSet( @@ -292,17 +283,12 @@ def create_ovphysx_articulation( mock_perm_wrench = WrenchComposer(articulation) object.__setattr__(articulation, "_instantaneous_wrench_composer", mock_inst_wrench) object.__setattr__(articulation, "_permanent_wrench_composer", mock_perm_wrench) - object.__setattr__(articulation, "_effort_write_view", None) - object.__setattr__(articulation, "_pos_target_write_view", None) - object.__setattr__(articulation, "_vel_target_write_view", None) - # Prevent __del__ / _clear_callbacks from raising object.__setattr__(articulation, "_initialize_handle", None) object.__setattr__(articulation, "_invalidate_initialize_handle", None) object.__setattr__(articulation, "_prim_deletion_handle", None) object.__setattr__(articulation, "_debug_vis_handle", None) - object.__setattr__(articulation, "actuators", {}) - object.__setattr__(articulation, "_has_implicit_actuators", False) + articulation._process_actuators_cfg() from isaaclab_ov import tensor_types as TT @@ -391,6 +377,7 @@ def create_newton_articulation( joint_ordering=joint_ordering, body_ordering=body_ordering, ) + object.__setattr__(articulation, "_sim_cfg", None) object.__setattr__(articulation, "_root_view", mock_view) object.__setattr__(articulation, "_device", device) @@ -415,10 +402,6 @@ def create_newton_articulation( object.__setattr__(articulation, "_prim_deletion_handle", None) object.__setattr__(articulation, "_debug_vis_handle", None) - # Other required attributes - object.__setattr__(articulation, "actuators", {}) - object.__setattr__(articulation, "_has_implicit_actuators", False) - # Newton uses wp.array for indices (not torch) object.__setattr__(articulation, "_ALL_INDICES", wp.array(np.arange(num_instances, dtype=np.int32), device=device)) object.__setattr__( @@ -450,22 +433,7 @@ def create_newton_articulation( ) object.__setattr__(articulation, "_ALL_SPATIAL_TENDON_MASK", wp.ones((0,), dtype=wp.bool, device=device)) - # Joint targets (Newton uses warp, not torch) - object.__setattr__( - articulation, - "_joint_pos_target_sim", - wp.zeros((num_instances, num_joints), dtype=wp.float32, device=device), - ) - object.__setattr__( - articulation, - "_joint_vel_target_sim", - wp.zeros((num_instances, num_joints), dtype=wp.float32, device=device), - ) - object.__setattr__( - articulation, - "_joint_effort_target_sim", - wp.zeros((num_instances, num_joints), dtype=wp.float32, device=device), - ) + articulation._process_actuators_cfg() return articulation, mock_view diff --git a/source/isaaclab/test/assets/test_articulation_iface.py b/source/isaaclab/test/assets/test_articulation_iface.py index c9c187b25835..f4e5b41b1d0a 100644 --- a/source/isaaclab/test/assets/test_articulation_iface.py +++ b/source/isaaclab/test/assets/test_articulation_iface.py @@ -13,6 +13,8 @@ The setup is a bit convoluted so that we can run these tests without requiring Isaac Sim or GPU simulation. """ +import warnings + import numpy as np import pytest import torch @@ -1179,6 +1181,32 @@ def test_joint_effort_target(self, backend, num_instances, num_joints, num_bodie name="joint_effort_target", ) + @_production_backends + @pytest.mark.parametrize("num_instances, num_joints, num_bodies", [(2, 4, 5)]) + @pytest.mark.parametrize("device", ["cpu"]) + def test_actuator_compatibility_projections_are_stable( + self, backend, num_instances, num_joints, num_bodies, device, articulation_iface + ): + art, _ = articulation_iface + soft_joint_vel_limits = torch.tensor([[0.5, 1.0, 1.5, 2.0], [2.5, 3.0, 3.5, 4.0]], dtype=torch.float32) + wp.copy(art.actuators._soft_joint_vel_limits, wp.from_torch(soft_joint_vel_limits)) + + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") + soft_joint_vel_limits_data = art.data.soft_joint_vel_limits + soft_joint_vel_limits_repeat = art.data.soft_joint_vel_limits + + _check_proxy_array( + soft_joint_vel_limits_data, + expected_shape=(num_instances, num_joints), + expected_dtype=wp.float32, + name="soft_joint_vel_limits", + ) + torch.testing.assert_close(soft_joint_vel_limits_data.torch, soft_joint_vel_limits, rtol=0.0, atol=0.0) + assert soft_joint_vel_limits_data.warp.ptr == soft_joint_vel_limits_repeat.warp.ptr + assert soft_joint_vel_limits_data.warp.ptr == art.actuators._soft_joint_vel_limits.ptr + assert not [warning for warning in caught_warnings if warning.category is DeprecationWarning] + # --------------------------------------------------------------------------- # Writer/setter test helpers diff --git a/source/isaaclab/test/assets/test_articulation_ordering_iface.py b/source/isaaclab/test/assets/test_articulation_ordering_iface.py index e88da29eddcb..85aef24c6d76 100644 --- a/source/isaaclab/test/assets/test_articulation_ordering_iface.py +++ b/source/isaaclab/test/assets/test_articulation_ordering_iface.py @@ -1843,7 +1843,7 @@ def test_ovphysx_partial_effort_target_write_preserves_unselected_backend_rows(s device="cpu", joint_ordering=_joint_ordering_for_mode("reversed", num_joints), ) - art._effort_write_view = object() + object.__setattr__(art, "_can_write_effort", True) user_to_backend = _ordering_user_to_backend(art.joint_ordering, num_joints) # Persist raw effort targets through the public setter, in backend order via the @@ -1973,6 +1973,18 @@ def _make_item_mask(total: int, selected: list[int], device: str) -> wp.array: class TestArticulationOperations: """Test cross-cutting articulation operations.""" + @_non_mock_backends + def test_legacy_position_target_accepts_joint_slice(self, backend: str) -> None: + art, _ = get_articulation(backend, num_instances=2, num_joints=4, num_bodies=2, device="cpu") + target = torch.tensor([[11.0, 12.0], [21.0, 22.0]], dtype=torch.float32) + + with pytest.warns(DeprecationWarning): + art.set_joint_position_target(target, joint_ids=slice(1, 3)) + + expected = torch.zeros((2, 4), dtype=torch.float32) + expected[:, 1:3] = target + torch.testing.assert_close(art.actuators.target_command.position.torch, expected) + @_non_mock_backends @pytest.mark.parametrize("ordering_mode", ["none", "reversed", "cyclic"]) @pytest.mark.parametrize("is_fixed_base", [False, True], ids=["floating", "fixed"]) @@ -2064,9 +2076,12 @@ def test_ovphysx_implicit_targets_are_written_in_backend_order(self, ordering_mo ) position = np.arange(num_instances * num_joints, dtype=np.float32).reshape(num_instances, num_joints) velocity = position + 100.0 + effort = position + 200.0 art.data._joint_pos_target.assign(wp.array(position, dtype=wp.float32, device=art.device)) art.data._joint_vel_target.assign(wp.array(velocity, dtype=wp.float32, device=art.device)) + art.data._applied_torque.assign(wp.array(effort, dtype=wp.float32, device=art.device)) object.__setattr__(art, "_has_implicit_actuators", True) + object.__setattr__(art, "_can_write_effort", True) art.write_data_to_sim() @@ -2079,6 +2094,47 @@ def test_ovphysx_implicit_targets_are_written_in_backend_order(self, ordering_mo raw_backend.bindings[TT.DOF_VELOCITY_TARGET]._data, velocity[:, backend_to_user], ) + np.testing.assert_array_equal( + raw_backend.bindings[TT.DOF_ACTUATION_FORCE]._data, + effort[:, backend_to_user], + ) + + @_requires_ovphysx + @pytest.mark.parametrize("selector_kind", ["torch", "warp"]) + def test_ovphysx_int64_effort_target_selector_reaches_binding( + self, selector_kind: str, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Convert indexed effort-target environment selectors for the OVPhysX binding.""" + from isaaclab_ov import tensor_types as TT + + art, raw_backend = get_articulation("ovphysx", 2, 3, 2, device="cpu") + object.__setattr__(art, "_can_write_effort", True) + expected = raw_backend.bindings[TT.DOF_ACTUATION_FORCE]._data.copy() + captured_indices = None + set_attribute = art._root_view.set_attribute + + def strict_set_attribute(name, values, *, indices=None, mask=None): + nonlocal captured_indices + if name == TT.DOF_ACTUATION_FORCE: + assert isinstance(indices, wp.array) + assert indices.dtype == wp.int32 + assert str(indices.device) == art.device + captured_indices = indices + set_attribute(name, values, indices=indices, mask=mask) + + monkeypatch.setattr(art._root_view, "set_attribute", strict_set_attribute) + env_ids_torch = torch.tensor([1], dtype=torch.int64) + env_ids = env_ids_torch if selector_kind == "torch" else wp.from_torch(env_ids_torch, dtype=wp.int64) + joint_ids = torch.tensor([2], dtype=torch.int64) + target = torch.tensor([[7.0]], dtype=torch.float32) + + art.set_joint_effort_target_index(target=target, env_ids=env_ids, joint_ids=joint_ids) + + expected[1] = 0.0 + expected[1, 2] = 7.0 + assert captured_indices is not None + np.testing.assert_array_equal(captured_indices.numpy(), [1]) + np.testing.assert_array_equal(raw_backend.bindings[TT.DOF_ACTUATION_FORCE]._data, expected) @_requires_physx @pytest.mark.parametrize("ordering_mode", ["reversed", "cyclic"]) @@ -2105,7 +2161,6 @@ def test_physx_newton_actuator_forces_are_written_in_backend_order(self, orderin object.__setattr__(art, "_physx_actuator_wrapper", wrapper) object.__setattr__(art, "_has_newton_actuators", True) object.__setattr__(art, "_has_implicit_actuators", False) - art._apply_actuator_model_newton = MagicMock() captured = {} def _capture_forces(forces, indices): @@ -2119,50 +2174,6 @@ def _capture_forces(forces, indices): backend_to_user = _expected_backend_to_user(joint_ordering, backend_joint_names) np.testing.assert_allclose(captured["forces"], user_forces_np[:, backend_to_user]) - @pytest.mark.parametrize( - ("method_name", "value_name", "controller_attr"), - [ - ("write_actuator_stiffness_to_sim", "stiffness", "kp"), - ("write_actuator_damping_to_sim", "damping", "kd"), - ], - ) - @_requires_physx - def test_physx_newton_actuator_gain_updates_use_public_joint_ids( - self, method_name: str, value_name: str, controller_attr: str - ): - """Route PhysX Newton-actuator gain updates by public joint ID.""" - art, _ = get_articulation( - "physx", - 1, - 3, - 2, - device="cpu", - joint_ordering=("joint_2", "joint_1", "joint_0"), - ) - controller = MagicMock( - kp=wp.array([1.0, 2.0, 3.0], dtype=wp.float32, device="cpu"), - kd=wp.array([1.0, 2.0, 3.0], dtype=wp.float32, device="cpu"), - ) - actuator = MagicMock( - controller=controller, - indices=wp.array([0, 1, 2], dtype=wp.uint32, device="cpu"), - ) - adapter = MagicMock(actuators=[actuator]) - object.__setattr__(art, "newton_actuator_adapter", adapter) - - getattr(art, method_name)( - **{ - value_name: torch.tensor([[99.0]], dtype=torch.float32), - "env_ids": torch.tensor([0], dtype=torch.int32), - "joint_ids": torch.tensor([0], dtype=torch.int32), - } - ) - - np.testing.assert_array_equal( - getattr(controller, controller_attr).numpy(), - np.asarray([99.0, 2.0, 3.0], dtype=np.float32), - ) - @_requires_physx def test_physx_validate_cfg_reports_velocity_limits_in_public_joint_order(self): """Pair public default velocities with limits for the same named joint.""" diff --git a/source/isaaclab/test/assets/test_articulation_ordering_kernels.py b/source/isaaclab/test/assets/test_articulation_ordering_kernels.py index fb7318ef85b8..0948e64d6e93 100644 --- a/source/isaaclab/test/assets/test_articulation_ordering_kernels.py +++ b/source/isaaclab/test/assets/test_articulation_ordering_kernels.py @@ -10,12 +10,12 @@ import warp as wp from isaaclab.assets.articulation.ordering_kernels import ( + launch_reorder_joint_targets_user_to_backend, reorder_2d_backend_to_user, reorder_2d_user_to_backend, reorder_3d_backend_to_user, reorder_body_state_backend_to_user, reorder_joint_state_backend_to_user, - reorder_joint_targets_user_to_backend, write_2d_user_to_backend_with_indices, write_2d_user_to_backend_with_mask, write_3d_user_to_backend_with_indices, @@ -176,7 +176,7 @@ def test_reorder_joint_targets_user_to_backend_writes_only_flagged_outputs( write_vel: bool, write_act: bool, ) -> None: - """Fuse the write-path target reorders, honoring per-output flags over a non-trivial permutation.""" + """Write only enabled targets in backend joint order.""" user_effort_np = np.asarray([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=np.float32) user_pos_np = user_effort_np + 10.0 user_vel_np = user_effort_np + 20.0 @@ -198,11 +198,19 @@ def test_reorder_joint_targets_user_to_backend_writes_only_flagged_outputs( backend_vel = wp.array(vel_sentinel, dtype=wp.float32, device="cpu") backend_act = wp.array(act_sentinel, dtype=wp.float32, device="cpu") - wp.launch( - reorder_joint_targets_user_to_backend, - dim=(user_effort_np.shape[0], user_effort_np.shape[1]), - inputs=[user_effort, user_pos, user_vel, backend_to_user, write_effort, write_pos, write_vel, write_act], - outputs=[backend_effort, backend_pos, backend_vel, backend_act], + launch_reorder_joint_targets_user_to_backend( + user_effort=user_effort, + user_pos_target=user_pos, + user_vel_target=user_vel, + backend_to_user=backend_to_user, + write_effort=write_effort, + write_pos_target=write_pos, + write_vel_target=write_vel, + write_joint_act=write_act, + backend_effort=backend_effort, + backend_pos_target=backend_pos, + backend_vel_target=backend_vel, + backend_joint_act=backend_act, device="cpu", ) diff --git a/source/isaaclab/test/utils/test_articulation_ordering.py b/source/isaaclab/test/utils/test_articulation_ordering.py index 6a5bd488bfa9..a2afe036f5f5 100644 --- a/source/isaaclab/test/utils/test_articulation_ordering.py +++ b/source/isaaclab/test/utils/test_articulation_ordering.py @@ -27,8 +27,8 @@ def _make_trace( "adapter_joint_names": adapter_joint_names, "joint_pos": [values], "joint_vel": [values], - "computed_torque": [values], - "applied_torque": [values], + "computed_effort": [values], + "applied_effort": [values], "adapter_computed_effort": [adapter_values], "adapter_applied_effort": [adapter_values], "target_pos": target_values, diff --git a/source/isaaclab/test/utils/test_wrench_composer.py b/source/isaaclab/test/utils/test_wrench_composer.py index c901de43e68e..b0a346eaa6c0 100644 --- a/source/isaaclab/test/utils/test_wrench_composer.py +++ b/source/isaaclab/test/utils/test_wrench_composer.py @@ -1173,8 +1173,9 @@ def test_partial_reset_zeros_only_specified_envs(device: str): @pytest.mark.parametrize("device", test_devices()) -def test_full_reset_clears_active_flag(device: str): - """Test that full reset (no args) clears the _active flag.""" +@pytest.mark.parametrize("env_ids", [None, slice(None)], ids=["none", "full_slice"]) +def test_full_reset_clears_active_flag(device: str, env_ids: slice | None): + """Test that either full-reset selector clears the _active flag.""" num_envs, num_bodies = 4, 2 mock_asset = create_mock_asset(num_envs, num_bodies, device) @@ -1186,7 +1187,7 @@ def test_full_reset_clears_active_flag(device: str): ) assert composer.active - composer.reset() + composer.reset(env_ids=env_ids) assert not composer.active assert not composer._dirty diff --git a/source/isaaclab/test/utils/warp/test_launch_cache.py b/source/isaaclab/test/utils/warp/test_launch_cache.py index c6681b67ade9..4a86a26e8c95 100644 --- a/source/isaaclab/test/utils/warp/test_launch_cache.py +++ b/source/isaaclab/test/utils/warp/test_launch_cache.py @@ -38,9 +38,15 @@ def launch(*args, **kwargs): assert calls[0]["record_cmd"] is True assert command.launches == 2 + cache.launch("other", kernel, dim=1, inputs=[], outputs=[]) + cache.clear("read") + cache.launch("read", kernel, dim=1, inputs=[], outputs=[]) + cache.launch("other", kernel, dim=1, inputs=[], outputs=[]) + assert len(calls) == 3 + cache.clear() cache.launch("read", kernel, dim=1, inputs=[], outputs=[]) - assert len(calls) == 2 + assert len(calls) == 4 device.is_capturing = True cache.launch("capture", kernel, dim=1, inputs=[], outputs=[]) @@ -54,4 +60,4 @@ def launch(*args, **kwargs): result[0] = None cache.launch("empty", kernel, dim=0, inputs=[], outputs=[]) cache.launch("empty", kernel, dim=0, inputs=[], outputs=[]) - assert len(calls) == 6 + assert len(calls) == 8 diff --git a/source/isaaclab_assets/changelog.d/actuator-collection.skip b/source/isaaclab_assets/changelog.d/actuator-collection.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab_assets/isaaclab_assets/robots/agibot.py b/source/isaaclab_assets/isaaclab_assets/robots/agibot.py index c5483721d2e0..b7b54bd08e45 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/agibot.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/agibot.py @@ -84,38 +84,38 @@ # Body lift and torso actuators "body": ImplicitActuatorCfg( joint_names_expr=["joint_lift_body", "joint_body_pitch"], - effort_limit_sim=10000.0, - velocity_limit_sim=2.61, + joint_effort_limit=10000.0, + joint_velocity_limit=2.61, stiffness=10000000.0, damping=200.0, ), # Head actuators "head": ImplicitActuatorCfg( joint_names_expr=["joint_head_yaw", "joint_head_pitch"], - effort_limit_sim=50.0, - velocity_limit_sim=1.0, + joint_effort_limit=50.0, + joint_velocity_limit=1.0, stiffness=80.0, damping=4.0, ), # Left arm actuator "left_arm": ImplicitActuatorCfg( joint_names_expr=["left_arm_joint[1-7]"], - effort_limit_sim={ + joint_effort_limit={ "left_arm_joint1": 2000.0, "left_arm_joint[2-7]": 1000.0, }, - velocity_limit_sim=1.57, + joint_velocity_limit=1.57, stiffness={"left_arm_joint1": 10000000.0, "left_arm_joint[2-7]": 20000.0}, damping={"left_arm_joint1": 0.0, "left_arm_joint[2-7]": 0.0}, ), # Right arm actuator "right_arm": ImplicitActuatorCfg( joint_names_expr=["right_arm_joint[1-7]"], - effort_limit_sim={ + joint_effort_limit={ "right_arm_joint1": 2000.0, "right_arm_joint[2-7]": 1000.0, }, - velocity_limit_sim=1.57, + joint_velocity_limit=1.57, stiffness={"right_arm_joint1": 10000000.0, "right_arm_joint[2-7]": 20000.0}, damping={"right_arm_joint1": 0.0, "right_arm_joint[2-7]": 0.0}, ), @@ -125,16 +125,16 @@ # set stiffness and damping to 0.0 below "left_gripper": ImplicitActuatorCfg( joint_names_expr=["left_hand_joint1", "left_.*_Support_Joint"], - effort_limit_sim={"left_hand_joint1": 10.0, "left_.*_Support_Joint": 1.0}, - velocity_limit_sim=2.0, + joint_effort_limit={"left_hand_joint1": 10.0, "left_.*_Support_Joint": 1.0}, + joint_velocity_limit=2.0, stiffness={"left_hand_joint1": 20.0, "left_.*_Support_Joint": 2.0}, damping={"left_hand_joint1": 0.10, "left_.*_Support_Joint": 0.01}, ), # set PD to zero for passive joints in close-loop gripper "left_gripper_passive": ImplicitActuatorCfg( joint_names_expr=["left_.*_(0|1)_Joint", "left_.*_RevoluteJoint"], - effort_limit_sim=10.0, - velocity_limit_sim=10.0, + joint_effort_limit=10.0, + joint_velocity_limit=10.0, stiffness=0.0, damping=0.0, ), @@ -144,16 +144,16 @@ # set stiffness and damping to 0.0 below "right_gripper": ImplicitActuatorCfg( joint_names_expr=["right_hand_joint1", "right_.*_Support_Joint"], - effort_limit_sim={"right_hand_joint1": 100.0, "right_.*_Support_Joint": 100.0}, - velocity_limit_sim=10.0, + joint_effort_limit={"right_hand_joint1": 100.0, "right_.*_Support_Joint": 100.0}, + joint_velocity_limit=10.0, stiffness={"right_hand_joint1": 20.0, "right_.*_Support_Joint": 2.0}, damping={"right_hand_joint1": 0.10, "right_.*_Support_Joint": 0.01}, ), # set PD to zero for passive joints in close-loop gripper "right_gripper_passive": ImplicitActuatorCfg( joint_names_expr=["right_.*_(0|1)_Joint", "right_.*_RevoluteJoint"], - effort_limit_sim=100.0, - velocity_limit_sim=10.0, + joint_effort_limit=100.0, + joint_velocity_limit=10.0, stiffness=0.0, damping=0.0, ), diff --git a/source/isaaclab_assets/isaaclab_assets/robots/allegro.py b/source/isaaclab_assets/isaaclab_assets/robots/allegro.py index b2669f2dd283..a413d7855d2f 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/allegro.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/allegro.py @@ -57,7 +57,7 @@ actuators={ "fingers": ImplicitActuatorCfg( joint_names_expr=[".*"], - effort_limit_sim=0.5, + joint_effort_limit=0.5, stiffness=3.0, damping=0.1, friction=0.01, diff --git a/source/isaaclab_assets/isaaclab_assets/robots/ant.py b/source/isaaclab_assets/isaaclab_assets/robots/ant.py index 97695e76edd9..4f51404d1992 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/ant.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/ant.py @@ -50,7 +50,7 @@ stiffness=0.0, damping=0.1, armature=0.05, - effort_limit_sim=15.0, + joint_effort_limit=15.0, ), }, ) diff --git a/source/isaaclab_assets/isaaclab_assets/robots/anymal.py b/source/isaaclab_assets/isaaclab_assets/robots/anymal.py index 18beaccc5d92..3d486b46fede 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/anymal.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/anymal.py @@ -34,8 +34,8 @@ ANYDRIVE_3_SIMPLE_ACTUATOR_CFG = DCMotorCfg( joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], saturation_effort=120.0, - effort_limit=80.0, - velocity_limit=7.5, + actuator_effort_limit=80.0, + actuator_velocity_limit=7.5, stiffness={".*": 40.0}, damping={".*": 5.0}, ) @@ -46,8 +46,8 @@ joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], network_file=f"{ISAACLAB_NUCLEUS_DIR}/ActuatorNets/ANYbotics/anydrive_3_lstm_jit.pt", saturation_effort=120.0, - effort_limit=80.0, - velocity_limit=7.5, + actuator_effort_limit=80.0, + actuator_velocity_limit=7.5, ) """Configuration for ANYdrive 3.0 (used on ANYmal-C) with LSTM actuator model.""" diff --git a/source/isaaclab_assets/isaaclab_assets/robots/cart_double_pendulum.py b/source/isaaclab_assets/isaaclab_assets/robots/cart_double_pendulum.py index 22028f39baf2..d96b2cffa90b 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/cart_double_pendulum.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/cart_double_pendulum.py @@ -38,15 +38,15 @@ actuators={ "cart_actuator": ImplicitActuatorCfg( joint_names_expr=["slider_to_cart"], - effort_limit_sim=400.0, + joint_effort_limit=400.0, stiffness=0.0, damping=10.0, ), "pole_actuator": ImplicitActuatorCfg( - joint_names_expr=["cart_to_pole"], effort_limit_sim=400.0, stiffness=0.0, damping=0.0 + joint_names_expr=["cart_to_pole"], joint_effort_limit=400.0, stiffness=0.0, damping=0.0 ), "pendulum_actuator": ImplicitActuatorCfg( - joint_names_expr=["pole_to_pendulum"], effort_limit_sim=400.0, stiffness=0.0, damping=0.0 + joint_names_expr=["pole_to_pendulum"], joint_effort_limit=400.0, stiffness=0.0, damping=0.0 ), }, ) diff --git a/source/isaaclab_assets/isaaclab_assets/robots/cartpole.py b/source/isaaclab_assets/isaaclab_assets/robots/cartpole.py index 1e236eda6b93..10c660c688f9 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/cartpole.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/cartpole.py @@ -38,12 +38,12 @@ actuators={ "cart_actuator": ImplicitActuatorCfg( joint_names_expr=["slider_to_cart"], - effort_limit_sim=400.0, + joint_effort_limit=400.0, stiffness=0.0, damping=10.0, ), "pole_actuator": ImplicitActuatorCfg( - joint_names_expr=["cart_to_pole"], effort_limit_sim=400.0, stiffness=0.0, damping=0.0 + joint_names_expr=["cart_to_pole"], joint_effort_limit=400.0, stiffness=0.0, damping=0.0 ), }, ) diff --git a/source/isaaclab_assets/isaaclab_assets/robots/cassie.py b/source/isaaclab_assets/isaaclab_assets/robots/cassie.py index faaefc414164..5916e4369945 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/cassie.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/cassie.py @@ -61,7 +61,7 @@ actuators={ "legs": ImplicitActuatorCfg( joint_names_expr=["hip_.*", "thigh_.*", "ankle_.*"], - effort_limit_sim=200.0, + joint_effort_limit=200.0, stiffness={ "hip_abduction.*": 100.0, "hip_rotation.*": 100.0, @@ -79,7 +79,7 @@ ), "toes": ImplicitActuatorCfg( joint_names_expr=["toe_.*"], - effort_limit_sim=20.0, + joint_effort_limit=20.0, stiffness={ "toe_joint.*": 20.0, }, diff --git a/source/isaaclab_assets/isaaclab_assets/robots/dr_legs.py b/source/isaaclab_assets/isaaclab_assets/robots/dr_legs.py index 21312ed75f5d..cf483aad857f 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/dr_legs.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/dr_legs.py @@ -101,8 +101,8 @@ joint_names_expr=DR_LEGS_ACTUATED_JOINTS, stiffness=5.0, damping=0.2, - effort_limit_sim=3.1, - velocity_limit_sim=100.0, + joint_effort_limit=3.1, + joint_velocity_limit=100.0, ), # Linkage DOFs are undriven: explicit zeros so the solver ignores USD drive defaults. "passive_joints": ImplicitActuatorCfg( @@ -111,8 +111,8 @@ damping=0.0, armature=0.0, friction=0.0, - effort_limit_sim=400.0, - velocity_limit_sim=100.0, + joint_effort_limit=400.0, + joint_velocity_limit=100.0, ), }, ) diff --git a/source/isaaclab_assets/isaaclab_assets/robots/flexiv.py b/source/isaaclab_assets/isaaclab_assets/robots/flexiv.py index 18adf4312022..a60d76023b79 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/flexiv.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/flexiv.py @@ -53,8 +53,8 @@ actuators={ "shoulder": ImplicitActuatorCfg( joint_names_expr=["joint[1-2]"], - effort_limit_sim=123.0, - velocity_limit_sim=2.094, + joint_effort_limit=123.0, + joint_velocity_limit=2.094, stiffness=6000.0, damping=108.5, friction=0.0, @@ -62,8 +62,8 @@ ), "elbow": ImplicitActuatorCfg( joint_names_expr=["joint[3-4]"], - effort_limit_sim=64.0, - velocity_limit_sim=2.443, + joint_effort_limit=64.0, + joint_velocity_limit=2.443, stiffness=4200.0, damping=90.7, friction=0.0, @@ -71,8 +71,8 @@ ), "wrist": ImplicitActuatorCfg( joint_names_expr=["joint[5-7]"], - effort_limit_sim=39.0, - velocity_limit_sim=4.887, + joint_effort_limit=39.0, + joint_velocity_limit=4.887, stiffness=1500.0, damping=54.2, friction=0.0, @@ -116,8 +116,8 @@ actuators={ "shoulder": ImplicitActuatorCfg( joint_names_expr=["joint[1-2]"], - effort_limit_sim=123.0, - velocity_limit_sim=2.094, + joint_effort_limit=123.0, + joint_velocity_limit=2.094, stiffness=1320.0, damping=72.0, friction=0.0, @@ -125,8 +125,8 @@ ), "elbow": ImplicitActuatorCfg( joint_names_expr=["joint[3-4]"], - effort_limit_sim=64.0, - velocity_limit_sim=2.443, + joint_effort_limit=64.0, + joint_velocity_limit=2.443, stiffness=600.0, damping=35.0, friction=0.0, @@ -134,8 +134,8 @@ ), "wrist": ImplicitActuatorCfg( joint_names_expr=["joint[5-7]"], - effort_limit_sim=39.0, - velocity_limit_sim=4.887, + joint_effort_limit=39.0, + joint_velocity_limit=4.887, stiffness=216.0, damping=29.0, friction=0.0, @@ -143,8 +143,8 @@ ), "gripper_drive": ImplicitActuatorCfg( joint_names_expr=["finger_joint"], - effort_limit_sim=200.0, - velocity_limit_sim=0.6, + joint_effort_limit=200.0, + joint_velocity_limit=0.6, stiffness=2e3, damping=1e1, friction=0.0, @@ -152,8 +152,8 @@ ), "gripper_passive": ImplicitActuatorCfg( joint_names_expr=[".*_knuckle_joint"], - effort_limit_sim=1.0, - velocity_limit_sim=1.0, + joint_effort_limit=1.0, + joint_velocity_limit=1.0, stiffness=0.0, damping=0.0, friction=0.0, diff --git a/source/isaaclab_assets/isaaclab_assets/robots/fourbar_pole.py b/source/isaaclab_assets/isaaclab_assets/robots/fourbar_pole.py index a6b3b266f24f..83c0bfbabbf0 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/fourbar_pole.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/fourbar_pole.py @@ -46,7 +46,7 @@ actuators={ "fourbar_actuator": ImplicitActuatorCfg( joint_names_expr=["ground_to_crank"], - effort_limit_sim=400.0, + joint_effort_limit=400.0, stiffness=0.0, damping=10.0, ), @@ -55,7 +55,7 @@ # an actuated joint and preserve its user-specified joint position on resets. "pole_actuator": ImplicitActuatorCfg( joint_names_expr=["coupler_to_pole"], - effort_limit_sim=400.0, + joint_effort_limit=400.0, stiffness=0.0, damping=0.0, ), diff --git a/source/isaaclab_assets/isaaclab_assets/robots/fourier.py b/source/isaaclab_assets/isaaclab_assets/robots/fourier.py index 58e143d11885..48ee086a1f8c 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/fourier.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/fourier.py @@ -55,8 +55,6 @@ joint_names_expr=[ "head_.*", ], - effort_limit=None, - velocity_limit=None, stiffness=None, damping=None, ), @@ -64,8 +62,6 @@ joint_names_expr=[ "waist_.*", ], - effort_limit=None, - velocity_limit=None, stiffness=None, damping=None, ), @@ -75,8 +71,6 @@ ".*_knee_.*", ".*_ankle_.*", ], - effort_limit=None, - velocity_limit=None, stiffness=None, damping=None, ), @@ -86,8 +80,8 @@ "right_elbow_.*", "right_wrist_.*", ], - effort_limit=torch.inf, - velocity_limit=torch.inf, + joint_effort_limit=torch.inf, + joint_velocity_limit=torch.inf, stiffness=None, damping=None, armature=0.0, @@ -98,8 +92,8 @@ "left_elbow_.*", "left_wrist_.*", ], - effort_limit=torch.inf, - velocity_limit=torch.inf, + joint_effort_limit=torch.inf, + joint_velocity_limit=torch.inf, stiffness=None, damping=None, armature=0.0, @@ -108,8 +102,6 @@ joint_names_expr=[ "R_.*", ], - effort_limit=None, - velocity_limit=None, stiffness=None, damping=None, ), @@ -117,8 +109,6 @@ joint_names_expr=[ "L_.*", ], - effort_limit=None, - velocity_limit=None, stiffness=None, damping=None, ), @@ -131,8 +121,6 @@ actuators={ "trunk": ImplicitActuatorCfg( joint_names_expr=["waist_.*"], - effort_limit=None, - velocity_limit=None, stiffness=4400, damping=40.0, armature=0.01, diff --git a/source/isaaclab_assets/isaaclab_assets/robots/franka.py b/source/isaaclab_assets/isaaclab_assets/robots/franka.py index 329bee6ff315..0340c413fd7b 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/franka.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/franka.py @@ -52,21 +52,21 @@ actuators={ "panda_shoulder": ImplicitActuatorCfg( joint_names_expr=["panda_joint[1-4]"], - effort_limit_sim=87.0, + joint_effort_limit=87.0, stiffness=80.0, damping=4.0, armature=1e-3, ), "panda_forearm": ImplicitActuatorCfg( joint_names_expr=["panda_joint[5-7]"], - effort_limit_sim=12.0, + joint_effort_limit=12.0, stiffness=80.0, damping=4.0, armature=1e-3, ), "panda_hand": ImplicitActuatorCfg( joint_names_expr=["panda_finger_joint.*"], - effort_limit_sim=200.0, + joint_effort_limit=200.0, stiffness=2e3, damping=1e2, ), @@ -81,7 +81,7 @@ FRANKA_PANDA_MENAGERIE_CFG.actuators = { "panda_arm": ImplicitActuatorCfg( joint_names_expr=["panda_joint[1-7]"], - velocity_limit_sim={"panda_joint[1-4]": 20.0, "panda_joint[5-7]": 25.0}, + joint_velocity_limit={"panda_joint[1-4]": 20.0, "panda_joint[5-7]": 25.0}, stiffness=None, damping=None, ), @@ -132,38 +132,38 @@ FRANKA_ROBOTIQ_GRIPPER_CFG.actuators = { "panda_shoulder": ImplicitActuatorCfg( joint_names_expr=["panda_joint[1-4]"], - effort_limit_sim=5200.0, - velocity_limit_sim=2.175, + joint_effort_limit=5200.0, + joint_velocity_limit=2.175, stiffness=1100.0, damping=80.0, ), "panda_forearm": ImplicitActuatorCfg( joint_names_expr=["panda_joint[5-7]"], - effort_limit_sim=720.0, - velocity_limit_sim=2.61, + joint_effort_limit=720.0, + joint_velocity_limit=2.61, stiffness=1000.0, damping=80.0, ), "gripper_drive": ImplicitActuatorCfg( joint_names_expr=["finger_joint"], # "right_outer_knuckle_joint" is its mimic joint - effort_limit_sim=1650, - velocity_limit_sim=10.0, + joint_effort_limit=1650, + joint_velocity_limit=10.0, stiffness=17, damping=0.02, ), # enable the gripper to grasp in a parallel manner "gripper_finger": ImplicitActuatorCfg( joint_names_expr=[".*_inner_finger_joint"], - effort_limit_sim=50, - velocity_limit_sim=10.0, + joint_effort_limit=50, + joint_velocity_limit=10.0, stiffness=0.2, damping=0.001, ), # set PD to zero for passive joints in close-loop gripper "gripper_passive": ImplicitActuatorCfg( joint_names_expr=[".*_inner_finger_knuckle_joint", "right_outer_knuckle_joint"], - effort_limit_sim=1.0, - velocity_limit_sim=10.0, + joint_effort_limit=1.0, + joint_velocity_limit=10.0, stiffness=0.0, damping=0.0, ), diff --git a/source/isaaclab_assets/isaaclab_assets/robots/galbot.py b/source/isaaclab_assets/isaaclab_assets/robots/galbot.py index 9827c7c8d31e..5cf1d378069a 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/galbot.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/galbot.py @@ -65,36 +65,36 @@ actuators={ "head": ImplicitActuatorCfg( joint_names_expr=["head_joint.*"], - velocity_limit_sim=None, - effort_limit_sim=None, + joint_velocity_limit=None, + joint_effort_limit=None, stiffness=None, damping=None, ), "leg": ImplicitActuatorCfg( joint_names_expr=["leg_joint.*"], - velocity_limit_sim=None, - effort_limit_sim=None, + joint_velocity_limit=None, + joint_effort_limit=None, stiffness=None, damping=None, ), "left_arm": ImplicitActuatorCfg( joint_names_expr=["left_arm_joint.*"], - velocity_limit_sim=None, - effort_limit_sim=None, + joint_velocity_limit=None, + joint_effort_limit=None, stiffness=None, damping=None, ), "right_arm": ImplicitActuatorCfg( joint_names_expr=["right_arm_joint.*", "right_suction_cup_joint1"], - velocity_limit_sim=None, - effort_limit_sim=None, + joint_velocity_limit=None, + joint_effort_limit=None, stiffness=None, damping=None, ), "left_gripper": ImplicitActuatorCfg( joint_names_expr=["left_gripper_.*_joint"], - velocity_limit_sim=1.0, - effort_limit_sim=None, + joint_velocity_limit=1.0, + joint_effort_limit=None, stiffness=None, damping=None, ), diff --git a/source/isaaclab_assets/isaaclab_assets/robots/humanoid.py b/source/isaaclab_assets/isaaclab_assets/robots/humanoid.py index 3ac06535d34e..6f58b36cb051 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/humanoid.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/humanoid.py @@ -63,9 +63,9 @@ ".*_shin": 0.1, ".*_foot.*": 1.0, }, - velocity_limit_sim={".*": 100.0}, + joint_velocity_limit={".*": 100.0}, armature={".*": 0.01}, - effort_limit_sim=150.0, + joint_effort_limit=150.0, ), }, ) diff --git a/source/isaaclab_assets/isaaclab_assets/robots/humanoid_28.py b/source/isaaclab_assets/isaaclab_assets/robots/humanoid_28.py index 84f44339a537..2006d895b060 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/humanoid_28.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/humanoid_28.py @@ -43,7 +43,7 @@ joint_names_expr=[".*"], stiffness=None, damping=None, - velocity_limit_sim={".*": 100.0}, + joint_velocity_limit={".*": 100.0}, ), }, ) diff --git a/source/isaaclab_assets/isaaclab_assets/robots/kinova.py b/source/isaaclab_assets/isaaclab_assets/robots/kinova.py index 3bef3896232e..96ed1930f87e 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/kinova.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/kinova.py @@ -51,7 +51,7 @@ actuators={ "arm": ImplicitActuatorCfg( joint_names_expr=[".*_joint_[1-7]"], - effort_limit_sim={ + joint_effort_limit={ ".*_joint_[1-2]": 80.0, ".*_joint_[3-4]": 40.0, ".*_joint_[5-7]": 20.0, @@ -67,7 +67,7 @@ ), "gripper": ImplicitActuatorCfg( joint_names_expr=[".*_finger_[1-3]", ".*_finger_tip_[1-3]"], - effort_limit_sim=2.0, + joint_effort_limit=2.0, stiffness=1.2, damping=0.01, ), @@ -103,7 +103,7 @@ actuators={ "arm": ImplicitActuatorCfg( joint_names_expr=[".*_joint_[1-6]"], - effort_limit_sim={ + joint_effort_limit={ ".*_joint_[1-2]": 80.0, ".*_joint_3": 40.0, ".*_joint_[4-6]": 20.0, @@ -119,7 +119,7 @@ ), "gripper": ImplicitActuatorCfg( joint_names_expr=[".*_finger_[1-3]", ".*_finger_tip_[1-3]"], - effort_limit_sim=2.0, + joint_effort_limit=2.0, stiffness=1.2, damping=0.01, ), @@ -154,7 +154,7 @@ actuators={ "arm": ImplicitActuatorCfg( joint_names_expr=["joint_[1-7]"], - effort_limit={ + joint_effort_limit={ "joint_[1-4]": 39.0, "joint_[5-7]": 9.0, }, diff --git a/source/isaaclab_assets/isaaclab_assets/robots/kuka_allegro.py b/source/isaaclab_assets/isaaclab_assets/robots/kuka_allegro.py index de4da2121e2f..54dc96fbfd35 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/kuka_allegro.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/kuka_allegro.py @@ -77,14 +77,14 @@ # https://github.com/RobotLocomotion/models/blob/master/iiwa_description/sdf/iiwa7_no_collision.sdf # https://www.wonikrobotics.com/robot-hand # https://github.com/RobotLocomotion/models/blob/master/allegro_hand_description/sdf/allegro_hand_description_right.sdf - effort_limit_sim={ + joint_effort_limit={ "iiwa7_joint_(1|2)": 176.0, "iiwa7_joint_(3|4|5)": 110.0, "iiwa7_joint_(6|7)": 40.0, "(index|middle|ring|thumb)_joint_(0|1|2|3)": 0.7, }, - # motor velocity limits for mdp checking — deliberately NOT velocity_limit_sim. - velocity_limit={ + # motor velocity limits for mdp checking — deliberately NOT joint_velocity_limit. + actuator_velocity_limit={ "iiwa7_joint_(1|2)": 1.7104, "iiwa7_joint_3": 1.7453, "iiwa7_joint_4": 2.2689, diff --git a/source/isaaclab_assets/isaaclab_assets/robots/openarm.py b/source/isaaclab_assets/isaaclab_assets/robots/openarm.py index 02743c5da915..befd5d3f163e 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/openarm.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/openarm.py @@ -65,7 +65,7 @@ "openarm_left_joint[1-7]", "openarm_right_joint[1-7]", ], - velocity_limit_sim={ + joint_velocity_limit={ "openarm_left_joint[1-2]": 2.175, "openarm_right_joint[1-2]": 2.175, "openarm_left_joint[3-4]": 2.175, @@ -73,7 +73,7 @@ "openarm_left_joint[5-7]": 2.61, "openarm_right_joint[5-7]": 2.61, }, - effort_limit_sim={ + joint_effort_limit={ "openarm_left_joint[1-2]": 40.0, "openarm_right_joint[1-2]": 40.0, "openarm_left_joint[3-4]": 27.0, @@ -89,8 +89,8 @@ "openarm_left_finger_joint.*", "openarm_right_finger_joint.*", ], - velocity_limit_sim=0.2, - effort_limit_sim=333.33, + joint_velocity_limit=0.2, + joint_effort_limit=333.33, stiffness=2e3, damping=1e2, ), @@ -127,12 +127,12 @@ actuators={ "openarm_arm": ImplicitActuatorCfg( joint_names_expr=["openarm_joint[1-7]"], - velocity_limit_sim={ + joint_velocity_limit={ "openarm_joint[1-2]": 2.175, "openarm_joint[3-4]": 2.175, "openarm_joint[5-7]": 2.61, }, - effort_limit_sim={ + joint_effort_limit={ "openarm_joint[1-2]": 40.0, "openarm_joint[3-4]": 27.0, "openarm_joint[5-7]": 7.0, @@ -142,8 +142,8 @@ ), "openarm_gripper": ImplicitActuatorCfg( joint_names_expr=["openarm_finger_joint.*"], - velocity_limit_sim=0.2, - effort_limit_sim=333.33, + joint_velocity_limit=0.2, + joint_effort_limit=333.33, stiffness=2e3, damping=1e2, ), diff --git a/source/isaaclab_assets/isaaclab_assets/robots/pick_and_place.py b/source/isaaclab_assets/isaaclab_assets/robots/pick_and_place.py index 988e042fcf65..8bcff4ef62ef 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/pick_and_place.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/pick_and_place.py @@ -45,22 +45,22 @@ actuators={ "x_gantry": ImplicitActuatorCfg( joint_names_expr=["x_axis"], - effort_limit=400.0, - velocity_limit=10.0, + joint_effort_limit=400.0, + joint_velocity_limit=10.0, stiffness=0.0, damping=10.0, ), "y_gantry": ImplicitActuatorCfg( joint_names_expr=["y_axis"], - effort_limit=400.0, - velocity_limit=10.0, + joint_effort_limit=400.0, + joint_velocity_limit=10.0, stiffness=0.0, damping=10.0, ), "z_gantry": ImplicitActuatorCfg( joint_names_expr=["z_axis"], - effort_limit=400.0, - velocity_limit=10.0, + joint_effort_limit=400.0, + joint_velocity_limit=10.0, stiffness=0.0, damping=10.0, ), diff --git a/source/isaaclab_assets/isaaclab_assets/robots/ridgeback_franka.py b/source/isaaclab_assets/isaaclab_assets/robots/ridgeback_franka.py index 312236d23373..010c0406a3cb 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/ridgeback_franka.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/ridgeback_franka.py @@ -49,25 +49,25 @@ actuators={ "base": ImplicitActuatorCfg( joint_names_expr=["dummy_base_.*"], - effort_limit_sim=1000.0, + joint_effort_limit=1000.0, stiffness=0.0, damping=1e5, ), "panda_shoulder": ImplicitActuatorCfg( joint_names_expr=["panda_joint[1-4]"], - effort_limit_sim=87.0, + joint_effort_limit=87.0, stiffness=800.0, damping=40.0, ), "panda_forearm": ImplicitActuatorCfg( joint_names_expr=["panda_joint[5-7]"], - effort_limit_sim=12.0, + joint_effort_limit=12.0, stiffness=800.0, damping=40.0, ), "panda_hand": ImplicitActuatorCfg( joint_names_expr=["panda_finger_joint.*"], - effort_limit_sim=200.0, + joint_effort_limit=200.0, stiffness=1e5, damping=1e3, ), diff --git a/source/isaaclab_assets/isaaclab_assets/robots/sawyer.py b/source/isaaclab_assets/isaaclab_assets/robots/sawyer.py index 179df09e7d81..cd89f83ba5dd 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/sawyer.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/sawyer.py @@ -48,13 +48,13 @@ actuators={ "head": ImplicitActuatorCfg( joint_names_expr=["head_pan"], - effort_limit_sim=8.0, + joint_effort_limit=8.0, stiffness=800.0, damping=40.0, ), "arm": ImplicitActuatorCfg( joint_names_expr=["right_j[0-6]"], - effort_limit_sim={ + joint_effort_limit={ "right_j[0-1]": 80.0, "right_j[2-3]": 40.0, "right_j[4-6]": 9.0, diff --git a/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py b/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py index 3d3c96876c2f..c13235648dd3 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py @@ -53,7 +53,7 @@ actuators={ "fingers": ImplicitActuatorCfg( joint_names_expr=["robot0_WR.*", "robot0_(FF|MF|RF|LF|TH)J(3|2|1)", "robot0_(LF|TH)J4", "robot0_THJ0"], - effort_limit_sim={ + joint_effort_limit={ "robot0_WRJ1": 4.785, "robot0_WRJ0": 2.175, "robot0_(FF|MF|RF|LF)J1": 0.7245, @@ -132,7 +132,7 @@ "robot0_(LF|TH)J4", "robot0_THJ0", ], - effort_limit_sim={ + joint_effort_limit={ "robot0_WRJ1": 4.785, "robot0_WRJ0": 2.175, "robot0_(FF|MF|RF|LF)J1": 0.7245, diff --git a/source/isaaclab_assets/isaaclab_assets/robots/so101.py b/source/isaaclab_assets/isaaclab_assets/robots/so101.py index aba5a180a33a..4edae5e7f704 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/so101.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/so101.py @@ -57,15 +57,15 @@ actuators={ "arm": ImplicitActuatorCfg( joint_names_expr=["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll"], - effort_limit_sim=10.0, - velocity_limit_sim=10.0, + joint_effort_limit=10.0, + joint_velocity_limit=10.0, stiffness=17.8, damping=0.60, ), "gripper": ImplicitActuatorCfg( joint_names_expr=["gripper"], - effort_limit_sim=10.0, - velocity_limit_sim=10.0, + joint_effort_limit=10.0, + joint_velocity_limit=10.0, stiffness=17.8, damping=0.60, ), diff --git a/source/isaaclab_assets/isaaclab_assets/robots/spot.py b/source/isaaclab_assets/isaaclab_assets/robots/spot.py index 3bc98b8b2da3..260575780d95 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/spot.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/spot.py @@ -162,7 +162,7 @@ actuators={ "spot_hip": DelayedPDActuatorCfg( joint_names_expr=[".*_h[xy]"], - effort_limit=45.0, + actuator_effort_limit=45.0, stiffness=60.0, damping=1.5, min_delay=0, # physics time steps (min: 2.0*0=0.0ms) @@ -171,7 +171,7 @@ "spot_knee": RemotizedPDActuatorCfg( joint_names_expr=[".*_kn"], joint_parameter_lookup=joint_parameter_lookup, - effort_limit=None, # torque limits are handled based experimental data (`RemotizedPDActuatorCfg.data`) + actuator_effort_limit=None, # Torque limits are based on experimental data (`RemotizedPDActuatorCfg.data`). stiffness=60.0, damping=1.5, min_delay=0, # physics time steps (min: 2.0*0=0.0ms) diff --git a/source/isaaclab_assets/isaaclab_assets/robots/unitree.py b/source/isaaclab_assets/isaaclab_assets/robots/unitree.py index cb72fcfe46ab..be6b2399efa9 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/unitree.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/unitree.py @@ -39,8 +39,8 @@ torque_scale=1.0, input_order="pos_vel", input_idx=[0, 1, 2], - effort_limit=23.7, # taken from spec sheet - velocity_limit=30.0, # taken from spec sheet + actuator_effort_limit=23.7, # taken from spec sheet + actuator_velocity_limit=30.0, # taken from spec sheet saturation_effort=23.7, # same as effort limit ) """Configuration of Go1 actuators using MLP model. @@ -88,9 +88,9 @@ actuators={ "base_legs": DCMotorCfg( joint_names_expr=[".*_hip_joint", ".*_thigh_joint", ".*_calf_joint"], - effort_limit=33.5, + actuator_effort_limit=33.5, saturation_effort=33.5, - velocity_limit=21.0, + actuator_velocity_limit=21.0, stiffness=25.0, damping=0.5, friction=0.0, @@ -171,9 +171,9 @@ actuators={ "base_legs": DCMotorCfg( joint_names_expr=[".*_hip_joint", ".*_thigh_joint", ".*_calf_joint"], - effort_limit=23.5, + actuator_effort_limit=23.5, saturation_effort=23.5, - velocity_limit=30.0, + actuator_velocity_limit=30.0, stiffness=25.0, damping=0.5, friction=0.0, @@ -220,7 +220,7 @@ actuators={ "legs": ImplicitActuatorCfg( joint_names_expr=[".*_hip_yaw", ".*_hip_roll", ".*_hip_pitch", ".*_knee", "torso"], - effort_limit_sim=300, + joint_effort_limit=300, stiffness={ ".*_hip_yaw": 150.0, ".*_hip_roll": 150.0, @@ -238,13 +238,13 @@ ), "feet": ImplicitActuatorCfg( joint_names_expr=[".*_ankle"], - effort_limit_sim=100, + joint_effort_limit=100, stiffness={".*_ankle": 20.0}, damping={".*_ankle": 4.0}, ), "arms": ImplicitActuatorCfg( joint_names_expr=[".*_shoulder_pitch", ".*_shoulder_roll", ".*_shoulder_yaw", ".*_elbow"], - effort_limit_sim=300, + joint_effort_limit=300, stiffness={ ".*_shoulder_pitch": 40.0, ".*_shoulder_roll": 40.0, @@ -316,7 +316,7 @@ ".*_knee_joint", "torso_joint", ], - effort_limit_sim=300, + joint_effort_limit=300, stiffness={ ".*_hip_yaw_joint": 150.0, ".*_hip_roll_joint": 150.0, @@ -338,7 +338,7 @@ }, ), "feet": ImplicitActuatorCfg( - effort_limit_sim=20, + joint_effort_limit=20, joint_names_expr=[".*_ankle_pitch_joint", ".*_ankle_roll_joint"], stiffness=20.0, damping=2.0, @@ -359,7 +359,7 @@ ".*_one_joint", ".*_two_joint", ], - effort_limit_sim=300, + joint_effort_limit=300, stiffness=40.0, damping=10.0, armature={ @@ -426,13 +426,13 @@ ".*_hip_pitch_joint", ".*_knee_joint", ], - effort_limit={ + actuator_effort_limit={ ".*_hip_yaw_joint": 88.0, ".*_hip_roll_joint": 88.0, ".*_hip_pitch_joint": 88.0, ".*_knee_joint": 139.0, }, - velocity_limit={ + actuator_velocity_limit={ ".*_hip_yaw_joint": 32.0, ".*_hip_roll_joint": 32.0, ".*_hip_pitch_joint": 32.0, @@ -466,11 +466,11 @@ ".*_ankle_pitch_joint": 0.2, ".*_ankle_roll_joint": 0.1, }, - effort_limit={ + actuator_effort_limit={ ".*_ankle_pitch_joint": 50.0, ".*_ankle_roll_joint": 50.0, }, - velocity_limit={ + actuator_velocity_limit={ ".*_ankle_pitch_joint": 37.0, ".*_ankle_roll_joint": 37.0, }, @@ -481,12 +481,12 @@ joint_names_expr=[ "waist_.*_joint", ], - effort_limit={ + joint_effort_limit={ "waist_yaw_joint": 88.0, "waist_roll_joint": 50.0, "waist_pitch_joint": 50.0, }, - velocity_limit={ + joint_velocity_limit={ "waist_yaw_joint": 32.0, "waist_roll_joint": 37.0, "waist_pitch_joint": 37.0, @@ -511,8 +511,8 @@ ".*_elbow_joint", ".*_wrist_.*_joint", ], - effort_limit=300, - velocity_limit=100, + joint_effort_limit=300, + joint_velocity_limit=100, stiffness=3000.0, damping=10.0, armature={ @@ -527,8 +527,8 @@ ".*_middle_.*", ".*_thumb_.*", ], - effort_limit=300, - velocity_limit=100, + joint_effort_limit=300, + joint_velocity_limit=100, stiffness=20, damping=2, armature=0.001, @@ -585,8 +585,8 @@ ".*_elbow_joint", ".*_wrist_.*_joint", ], - effort_limit=300, - velocity_limit=100, + joint_effort_limit=300, + joint_velocity_limit=100, stiffness=3000.0, damping=100.0, armature={ @@ -605,8 +605,8 @@ ".*_ring_.*", ".*_pinky_.*", ], - effort_limit_sim=30.0, - velocity_limit_sim=10.0, + joint_effort_limit=30.0, + joint_velocity_limit=10.0, stiffness=10.0, damping=0.2, armature=0.001, @@ -691,13 +691,13 @@ ".*_hip_pitch_joint", ".*_knee_joint", ], - effort_limit={ + actuator_effort_limit={ ".*_hip_yaw_joint": 88.0, ".*_hip_roll_joint": 88.0, ".*_hip_pitch_joint": 88.0, ".*_knee_joint": 139.0, }, - velocity_limit={ + actuator_velocity_limit={ ".*_hip_yaw_joint": 32.0, ".*_hip_roll_joint": 32.0, ".*_hip_pitch_joint": 32.0, @@ -730,11 +730,11 @@ ".*_ankle_pitch_joint": 2, ".*_ankle_roll_joint": 2, }, - effort_limit={ + actuator_effort_limit={ ".*_ankle_pitch_joint": 50.0, ".*_ankle_roll_joint": 50.0, }, - velocity_limit={ + actuator_velocity_limit={ ".*_ankle_pitch_joint": 37.0, ".*_ankle_roll_joint": 37.0, }, @@ -743,8 +743,8 @@ ), "waist": ImplicitActuatorCfg( joint_names_expr=["waist_yaw_joint", "waist_roll_joint", "waist_pitch_joint"], - effort_limit=1000.0, - velocity_limit=0.0, + joint_effort_limit=1000.0, + joint_velocity_limit=0.0, stiffness={"waist_yaw_joint": 10000.0, "waist_roll_joint": 10000.0, "waist_pitch_joint": 10000.0}, damping={"waist_yaw_joint": 10000.0, "waist_roll_joint": 10000.0, "waist_pitch_joint": 10000.0}, armature=None, @@ -757,7 +757,7 @@ ".*_elbow_joint", ".*_wrist_.*_joint", ], - effort_limit={ + actuator_effort_limit={ ".*_shoulder_pitch_joint": 25.0, ".*_shoulder_roll_joint": 25.0, ".*_shoulder_yaw_joint": 25.0, @@ -766,7 +766,7 @@ ".*_wrist_pitch_joint": 5.0, ".*_wrist_yaw_joint": 5.0, }, - velocity_limit={ + actuator_velocity_limit={ ".*_shoulder_pitch_joint": 37.0, ".*_shoulder_roll_joint": 37.0, ".*_shoulder_yaw_joint": 37.0, @@ -796,8 +796,8 @@ joint_names_expr=[ ".*_hand_.*", ], - effort_limit=5.0, - velocity_limit=10.0, + actuator_effort_limit=5.0, + actuator_velocity_limit=10.0, stiffness=8.0, damping=1.5, armature=0.03, diff --git a/source/isaaclab_assets/isaaclab_assets/robots/universal_robots.py b/source/isaaclab_assets/isaaclab_assets/robots/universal_robots.py index 9047fc14e764..a79dac519049 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/universal_robots.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/universal_robots.py @@ -46,7 +46,7 @@ actuators={ "arm": ImplicitActuatorCfg( joint_names_expr=[".*"], - effort_limit_sim=87.0, + joint_effort_limit=87.0, stiffness=800.0, damping=40.0, ), @@ -136,8 +136,8 @@ # the major actuator joint for gripper UR10e_ROBOTIQ_GRIPPER_CFG.actuators["gripper_drive"] = ImplicitActuatorCfg( joint_names_expr=["finger_joint"], - effort_limit_sim=10.0, - velocity_limit_sim=1.0, + joint_effort_limit=10.0, + joint_velocity_limit=1.0, stiffness=11.25, damping=0.1, friction=0.0, @@ -146,8 +146,8 @@ # the auxiliary actuator joint for gripper UR10e_ROBOTIQ_GRIPPER_CFG.actuators["gripper_finger"] = ImplicitActuatorCfg( joint_names_expr=[".*_inner_finger_joint"], - effort_limit_sim=1.0, - velocity_limit_sim=1.0, + joint_effort_limit=1.0, + joint_velocity_limit=1.0, stiffness=0.2, damping=0.001, friction=0.0, @@ -156,8 +156,8 @@ # the passive joints for gripper UR10e_ROBOTIQ_GRIPPER_CFG.actuators["gripper_passive"] = ImplicitActuatorCfg( joint_names_expr=[".*_inner_finger_pad_joint", ".*_outer_finger_joint", "right_outer_knuckle_joint"], - effort_limit_sim=1.0, - velocity_limit_sim=1.0, + joint_effort_limit=1.0, + joint_velocity_limit=1.0, stiffness=0.0, damping=0.0, friction=0.0, @@ -176,8 +176,8 @@ # the major actuator joint for gripper UR10e_ROBOTIQ_2F_85_CFG.actuators["gripper_drive"] = ImplicitActuatorCfg( joint_names_expr=["finger_joint"], # "right_outer_knuckle_joint" is its mimic joint - effort_limit_sim=10.0, - velocity_limit_sim=1.0, + joint_effort_limit=10.0, + joint_velocity_limit=1.0, stiffness=11.25, damping=0.1, friction=0.0, @@ -186,8 +186,8 @@ # enable the gripper to grasp in a parallel manner UR10e_ROBOTIQ_2F_85_CFG.actuators["gripper_finger"] = ImplicitActuatorCfg( joint_names_expr=[".*_inner_finger_joint"], - effort_limit_sim=1.0, - velocity_limit_sim=1.0, + joint_effort_limit=1.0, + joint_velocity_limit=1.0, stiffness=0.2, damping=0.001, friction=0.0, @@ -196,8 +196,8 @@ # set PD to zero for passive joints in close-loop gripper UR10e_ROBOTIQ_2F_85_CFG.actuators["gripper_passive"] = ImplicitActuatorCfg( joint_names_expr=[".*_inner_finger_knuckle_joint", "right_outer_knuckle_joint"], - effort_limit_sim=1.0, - velocity_limit_sim=1.0, + joint_effort_limit=1.0, + joint_velocity_limit=1.0, stiffness=0.0, damping=0.0, friction=0.0, diff --git a/source/isaaclab_contrib/changelog.d/actuator-collection.skip b/source/isaaclab_contrib/changelog.d/actuator-collection.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor.py b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor.py index 838fbb657025..0d8cebf7d7eb 100644 --- a/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor.py +++ b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor.py @@ -31,6 +31,26 @@ logger = logging.getLogger(__name__) +class _ThrusterCollection(dict): + """Name-keyed mapping of :class:`~isaaclab_contrib.actuators.Thruster` actuators. + + Multirotors are controlled through thrusters rather than joint actuators, so + :class:`Multirotor` stores its actuators in this lightweight ``dict`` subclass instead of a + joint-based :class:`~isaaclab.actuators.ActuatorCollection`. Behaving as a plain ``dict`` keeps + the existing name-based access (``self.actuators["thrusters"]``, iteration, ``.values()``) while + exposing the :meth:`reset` entry point that :meth:`isaaclab.assets.Articulation.reset` invokes. + """ + + def reset(self, env_ids: Sequence[int] | slice | None = None) -> None: + """Reset every thruster actuator for the given environments. + + Args: + env_ids: Environment indices to reset. Defaults to None (all environments). + """ + for actuator in self.values(): + actuator.reset(env_ids) + + class Multirotor(Articulation): """A multirotor articulation asset class. @@ -405,7 +425,7 @@ def _process_cfg(self): def _process_thruster_cfg(self): """Process and apply multirotor thruster properties.""" # create actuators - self.actuators = dict() + self.actuators = _ThrusterCollection() self._has_implicit_actuators = False # Check for mixed configurations (same as before) diff --git a/source/isaaclab_experimental/changelog.d/actuator-collection.skip b/source/isaaclab_experimental/changelog.d/actuator-collection.skip new file mode 100644 index 000000000000..0e099b9d50d5 --- /dev/null +++ b/source/isaaclab_experimental/changelog.d/actuator-collection.skip @@ -0,0 +1,3 @@ +Internal-only change: migrated experimental rewards to the actuator collection +telemetry API. Behavior is unchanged, so no user-facing changelog entry is +warranted. diff --git a/source/isaaclab_experimental/isaaclab_experimental/envs/mdp/rewards.py b/source/isaaclab_experimental/isaaclab_experimental/envs/mdp/rewards.py index 1eb07bc9c6c0..e4a8d6680191 100644 --- a/source/isaaclab_experimental/isaaclab_experimental/envs/mdp/rewards.py +++ b/source/isaaclab_experimental/isaaclab_experimental/envs/mdp/rewards.py @@ -231,7 +231,7 @@ def joint_torques_l2(env: ManagerBasedRLEnv, out, asset_cfg: SceneEntityCfg = Sc wp.launch( kernel=_sum_sq_masked_kernel, dim=env.num_envs, - inputs=[asset.data.applied_torque.warp, asset_cfg.joint_mask, out], + inputs=[asset.actuators.applied_effort.warp, asset_cfg.joint_mask, out], device=env.device, ) diff --git a/source/isaaclab_experimental/test/envs/mdp/parity_helpers.py b/source/isaaclab_experimental/test/envs/mdp/parity_helpers.py index 9da36f29bf5a..6cb14081390e 100644 --- a/source/isaaclab_experimental/test/envs/mdp/parity_helpers.py +++ b/source/isaaclab_experimental/test/envs/mdp/parity_helpers.py @@ -312,6 +312,10 @@ def __init__(self, data: MockArticulationData, num_bodies: int = 1, num_joints: self.num_joints = num_joints self.device = DEVICE self._joint_names = [f"joint_{i}" for i in range(num_joints)] + self.actuators = SimpleNamespace( + applied_effort=data.applied_torque, + computed_effort=data.computed_torque, + ) self.permanent_wrench_composer = MockWrenchComposer() # Tracking attributes for action tests self.last_pos_target = None diff --git a/source/isaaclab_newton/changelog.d/actuator-collection.minor.rst b/source/isaaclab_newton/changelog.d/actuator-collection.minor.rst new file mode 100644 index 000000000000..95f2c6b4c5ef --- /dev/null +++ b/source/isaaclab_newton/changelog.d/actuator-collection.minor.rst @@ -0,0 +1,20 @@ +Added +^^^^^ + +* Added explicit state-buffer advancement so Newton actuator adapters can be + replayed from backend-owned CUDA graphs. + +Changed +^^^^^^^ + +* Routed Newton articulation actuator setup, compute, reset, and command + submission through :class:`~isaaclab.actuators.ActuatorCollection`. +* Changed host-adapter execution of Newton actuators on PhysX and OVPhysX to + aggregate structurally compatible joints while retaining their per-joint + parameters. + +Fixed +^^^^^ + +* Fixed Newton-native execution with non-graphable actuators to keep solver + stepping in a CUDA graph. diff --git a/source/isaaclab_newton/isaaclab_newton/actuators/__init__.py b/source/isaaclab_newton/isaaclab_newton/actuators/__init__.py deleted file mode 100644 index aacc07f96666..000000000000 --- a/source/isaaclab_newton/isaaclab_newton/actuators/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -# 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-native actuator integration for Isaac Lab. - -Public API surface: - -* :class:`~isaaclab_newton.actuators.adapter.NewtonActuatorAdapter` — - the actuator adapter used by both backends. The Newton backend - constructs it directly from ``model.actuators``; the PhysX backend - uses :meth:`~NewtonActuatorAdapter.from_usd` to build the actuators - from authored ``NewtonActuator`` USD prims. -* :class:`~isaaclab_newton.actuators.physx_wrapper.PhysxActuatorWrapper` - — flat-array wrapper that satisfies the Newton actuator - ``sim_state`` / ``sim_control`` protocol on the PhysX backend. -* :func:`~isaaclab_newton.actuators.kernels.build_implicit_dof_mask` — - builds the per-DOF implicit-actuator mask consumed by the in-graph - post-actuator kernel. - -USD authoring lives on the schema side as -:func:`~isaaclab.sim.schemas.define_actuator_properties`; both backends -call into it via :meth:`ArticulationCfg._post_spawn`. -""" - -from .adapter import NewtonActuatorAdapter, build_newton_actuator_defaults -from .kernels import build_implicit_dof_mask -from .physx_wrapper import PhysxActuatorWrapper - -__all__ = [ - "NewtonActuatorAdapter", - "PhysxActuatorWrapper", - "build_implicit_dof_mask", - "build_newton_actuator_defaults", -] diff --git a/source/isaaclab_newton/isaaclab_newton/actuators/adapter.py b/source/isaaclab_newton/isaaclab_newton/actuators/adapter.py deleted file mode 100644 index 7f6fc64bf65d..000000000000 --- a/source/isaaclab_newton/isaaclab_newton/actuators/adapter.py +++ /dev/null @@ -1,599 +0,0 @@ -# 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-actuator adapter shared by the Newton and PhysX backends. - -Owns the actuator-state lifecycle, the pre-clamp computed-effort buffer, -and the per-step ``step`` / ``reset`` / ``finalize`` calls. The -:meth:`~NewtonActuatorAdapter.from_usd` classmethod parses -``NewtonActuator`` USD prims on the PhysX backend (Newton populates -``model.actuators`` itself). - -DR gain updates bypass the adapter — the articulation writes straight -to controller arrays. -""" - -from __future__ import annotations - -from collections.abc import Sequence -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any - -import numpy as np -import torch -import warp as wp -from newton.actuators import Actuator, Clamping, Delay - -from .kernels import ( - build_implicit_dof_mask, - build_per_dof_env_mask_kernel, - scatter_gain_kernel, - set_mask_kernel, - zero_at_indices_kernel, -) - -if TYPE_CHECKING: - from isaaclab.actuators import ActuatorBase - -# --------------------------------------------------------------------------- -# Abstract base — backend-independent logic -# --------------------------------------------------------------------------- - - -class NewtonActuatorAdapter: - """Adapter that wraps a list of :class:`newton.actuators.Actuator`. - - Owns the actuator-state lifecycle, DOF-to-actuator bookkeeping, - stepping, reset, and the pre-clamp computed-effort buffer the - in-graph telemetry kernel reads on the post-actuator hook. - """ - - @dataclass(frozen=True) - class ArticulationBinding: - """Newton fast-path init state for one articulation. - - Returned by :meth:`bind_articulation`. Bundles the pieces the - articulation formerly assembled from separate free-function calls: - the initial gain snapshot, the implicit-DOF mask, and the - per-articulation view of the adapter's computed-effort buffer. - """ - - stiffness: torch.Tensor - """Initial stiffness gains [N/m or N·m/rad, depending on joint type], shape ``(num_envs, num_joints)``.""" - - damping: torch.Tensor - """Initial damping gains [N·s/m or N·m·s/rad, depending on joint type], shape ``(num_envs, num_joints)``.""" - - joint_indices: torch.Tensor | slice - """Managed columns; ``slice(None)`` when every joint is managed, else a ``torch.int32`` index tensor.""" - - implicit_dof_mask: wp.array - """Per-DOF mask consumed by ``sync_torque_telemetry``; ``1`` on implicit-actuator DOFs, ``0`` otherwise.""" - - implicit_dof_mask_owner: torch.Tensor - """Torch tensor owning the memory :attr:`implicit_dof_mask` aliases; keep referenced for the mask's lifetime.""" - - computed_effort_view: wp.array - """This articulation's slice of the adapter's pre-clamp computed-effort buffer, ``(num_envs, num_joints)``.""" - - def __init__( - self, - actuators: list[Actuator], - num_envs: int, - num_joints: int, - dof_offset: int, - device: str, - ): - self.actuators = actuators - self.num_joints = num_joints - - self._num_envs = num_envs - self._dof_offset = dof_offset - self._device = device - - # Collect the set of local DOFs covered by some actuator. Only the - # env-0 slice of each actuator's flat ``indices`` array is needed — - # later envs are repeats with a constant ``num_joints`` stride. - managed: set[int] = set() - for act in actuators: - all_indices = act.indices.numpy() - num_per_act = len(all_indices) // num_envs - for global_dof in all_indices[:num_per_act]: - local_dof = global_dof - dof_offset - if 0 <= local_dof < num_joints: - managed.add(local_dof) - - if len(managed) == num_joints: - self.joint_indices: torch.Tensor | slice = slice(None) - else: - self.joint_indices = torch.tensor(sorted(managed), dtype=torch.int32, device=device) - - self._states_a = [act.state() for act in actuators] - self._states_b = [act.state() for act in actuators] - - # Pre-clamp computed effort buffer. Each Newton actuator scatter-adds - # its raw controller output to ``sim_control.joint_computed_f`` when - # ``control_computed_output_attr`` is set; we route that to this - # buffer so the post-actuator telemetry kernel can report the actual - # computed (pre-clamp) effort instead of mirroring ``joint_f``. The - # binding onto ``sim_control`` happens in :meth:`finalize`. - self._computed_effort = wp.zeros( - num_envs * num_joints, - dtype=wp.float32, - device=device, - ) - self.computed_effort_2d = self._computed_effort.reshape((num_envs, num_joints)) - for act in actuators: - act.control_computed_output_attr = "joint_computed_f" - - def finalize(self, sim_control: Any) -> None: - """Bind the pre-clamp computed-effort buffer onto ``sim_control``. - - Args: - sim_control: The ``sim_control`` object that will be passed - to :meth:`step` for this adapter's lifetime. Newton's - ``Control`` on the Newton backend, an - :class:`~isaaclab_newton.actuators.physx_wrapper.PhysxActuatorWrapper` - on the PhysX backend. - """ - sim_control.joint_computed_f = self._computed_effort - - def step(self, sim_state: Any, sim_control: Any, dt: float) -> None: - """Zero actuated DOFs, step all actuators, and swap state buffers. - - Args: - sim_state: Object with ``joint_q``, ``joint_qd``, etc. - Newton ``State`` on the Newton backend, - :class:`~isaaclab_newton.actuators.physx_wrapper.PhysxActuatorWrapper` - on the PhysX backend. - sim_control: Object with ``joint_f``, ``joint_target_q``, etc. - Newton ``Control`` on the Newton backend, - :class:`~isaaclab_newton.actuators.physx_wrapper.PhysxActuatorWrapper` - on the PhysX backend. - dt: Physics timestep [s]. - """ - # Zero before scatter-add (actuators accumulate into this buffer). - self._computed_effort.zero_() - for act in self.actuators: - wp.launch( - zero_at_indices_kernel, - dim=act.indices.shape[0], - inputs=[sim_control.joint_f, act.indices], - ) - for act, sa, sb in zip(self.actuators, self._states_a, self._states_b): - act.step(sim_state, sim_control, sa, sb, dt=dt) - self._states_a, self._states_b = self._states_b, self._states_a - - def reset(self, env_ids: Sequence[int] | torch.Tensor | None = None) -> None: - """Reset actuator states for the given environments. - - Args: - env_ids: Environment indices to reset. ``None`` (or - ``slice(None)``, which IsaacLab callers sometimes pass) - resets all environments. Otherwise expects a torch tensor - or sequence of int indices. - - Newton's :meth:`Actuator.State.reset` expects a per-DOF boolean - mask of length ``num_actuators`` (= ``num_envs * dofs_per_actuator``), - not a per-env mask — each entry gates the corresponding column of - the actuator's state buffers (delay queue, controller integral, - etc.). We therefore build a per-actuator per-DOF mask from the - env mask before delegating to each state. - """ - if env_ids is None or env_ids == slice(None): - for sa, sb in zip(self._states_a, self._states_b): - if sa is not None: - sa.reset(None) - if sb is not None: - sb.reset(None) - return - - if isinstance(env_ids, torch.Tensor): - if env_ids.numel() == 0: - return - idx = wp.from_torch(env_ids.to(device=self._device).contiguous().to(torch.int32), dtype=wp.int32) - else: - if len(env_ids) == 0: - return - idx = wp.array(list(env_ids), dtype=wp.int32, device=self._device) - env_mask = wp.zeros(self._num_envs, dtype=wp.bool, device=self._device) - wp.launch(set_mask_kernel, dim=idx.shape[0], inputs=[env_mask, idx], device=self._device) - - for act, sa, sb in zip(self.actuators, self._states_a, self._states_b): - per_dof_mask = wp.zeros(act.indices.shape[0], dtype=wp.bool, device=self._device) - wp.launch( - build_per_dof_env_mask_kernel, - dim=act.indices.shape[0], - inputs=[act.indices, env_mask, self._dof_offset, self.num_joints, per_dof_mask], - device=self._device, - ) - if sa is not None: - sa.reset(per_dof_mask) - if sb is not None: - sb.reset(per_dof_mask) - - def bind_articulation( - self, - *, - lab_actuators: dict[str, ActuatorBase], - dof_offset: int, - num_joints: int, - joint_user_to_backend_indices: Sequence[int] | None = None, - ) -> ArticulationBinding: - """Assemble the Newton fast-path init state for one articulation. - - Consolidates the pieces the articulation formerly built with - separate :func:`build_newton_actuator_defaults` and - :func:`build_implicit_dof_mask` calls plus a manual - computed-effort slice: it snapshots the initial gains, builds the - implicit-DOF mask, and slices this adapter's computed-effort buffer - to the articulation's columns. Whole-model quantities - (:attr:`actuators`, :attr:`num_joints` as the env stride, - ``num_envs``, ``device``) come from the adapter; only the - articulation-local placement varies per call. - - Args: - lab_actuators: The articulation's Isaac Lab actuator groups in - public joint order. Only :class:`~isaaclab.actuators.ImplicitActuator` - groups contribute to :attr:`ArticulationBinding.implicit_dof_mask`. - dof_offset: Offset of this articulation's DOFs in the adapter's - env-major global index space (``0`` on PhysX, view-dependent - on Newton). - num_joints: Articulation-local joint count. Distinct from - :attr:`num_joints`, which is the whole-model per-env DOF - stride used to lay out the actuator index arrays. - joint_user_to_backend_indices: Complete permutation from public - joint indices to adapter-local joint indices. ``None`` - preserves adapter-local order (the PhysX case, whose adapter - is already built from public joint names). - - Returns: - The bundled :class:`ArticulationBinding` for this articulation. - """ - stiffness, damping, joint_indices = build_newton_actuator_defaults( - actuators=self.actuators, - num_envs=self._num_envs, - num_joints=num_joints, - dof_offset=dof_offset, - env_stride=self.num_joints, - device=self._device, - joint_user_to_backend_indices=joint_user_to_backend_indices, - ) - implicit_dof_mask, implicit_dof_mask_owner = build_implicit_dof_mask(lab_actuators, num_joints, self._device) - computed_effort_view = self.computed_effort_2d[:, dof_offset : dof_offset + num_joints] - return self.ArticulationBinding( - stiffness=stiffness, - damping=damping, - joint_indices=joint_indices, - implicit_dof_mask=implicit_dof_mask, - implicit_dof_mask_owner=implicit_dof_mask_owner, - computed_effort_view=computed_effort_view, - ) - - @property - def is_all_graphable(self) -> bool: - """``True`` when all actuators are CUDA-graph-safe.""" - return len(self.actuators) > 0 and all(a.is_graphable() for a in self.actuators) - - @classmethod - def from_usd( - cls, - stage: Any, - joint_names: list[str], - num_envs: int, - num_joints: int, - device: str, - articulation_prim_path: str | None = None, - ) -> NewtonActuatorAdapter: - """Build an adapter from ``NewtonActuator`` prims authored on *stage*. - - This is the PhysX-side counterpart of Newton's - ``ModelBuilder.add_usd``. It reads the same prims and constructs matching - :class:`~newton.actuators.Actuator` objects. Joints with the same - controller, gains, clamping, and delay are merged into one actuator with - combined indices. Newton backends use ``model.actuators`` instead. - - On PhysX, :paramref:`joint_names` is in this adapter's local public order - and defines the local indices assigned to parsed actuator targets. - - Args: - stage: USD stage containing ``NewtonActuator`` prims. - joint_names: All articulation joint names in adapter-local public order. - num_envs: Number of environments. - num_joints: Number of joints per environment. - device: Warp device string, for example ``"cuda:0"``. - articulation_prim_path: Root prim path of environment zero's - articulation. When set, only prims under this subtree are - considered; otherwise the whole stage is scanned. - - Returns: - Adapter whose actuator indices use :paramref:`joint_names` order. - - Raises: - ValueError: If no authored actuator targets a name in - :paramref:`joint_names`. - """ - actuators = _create_actuators_from_usd( - stage, - joint_names, - num_envs, - num_joints, - device, - articulation_prim_path=articulation_prim_path, - ) - return cls(actuators, num_envs, num_joints, dof_offset=0, device=device) - - -# --------------------------------------------------------------------------- -# Per-articulation initial-gain snapshot — consumed by -# ``randomize_actuator_gains`` to seed ``default_joint_*`` baselines. -# --------------------------------------------------------------------------- - - -def build_newton_actuator_defaults( - actuators: list[Actuator], - num_envs: int, - num_joints: int, - dof_offset: int, - env_stride: int, - device: str, - joint_user_to_backend_indices: Sequence[int] | None = None, -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | slice]: - """Snapshot initial Newton actuator gains for one articulation. - - Actuators are filtered to those whose environment-zero DOF lies in - ``[dof_offset, dof_offset + num_joints)``. Their gains are scattered in the - actuator adapter's local joint order. Without :paramref:`joint_user_to_backend_indices`, - the output preserves that local order. PhysX builds its per-articulation adapter from - public joint names, so its adapter-local order is public order. Newton's global adapter - uses backend-local order; the optional map converts its gains and managed indices to - public order. - - Args: - actuators: Newton actuators visible to this articulation. - num_envs: Number of environments. - num_joints: Articulation-local joint count. - dof_offset: Offset of this articulation's DOFs in the env-major - global index space (``0`` on PhysX, view-dependent on Newton). - env_stride: Whole-model per-env DOF count — the stride used to build - each actuator's env-major ``indices``. Equals ``num_joints`` on - PhysX, but exceeds it by the free-root DOFs on a floating-base - Newton articulation, so it must be passed explicitly rather than - assumed equal to ``num_joints``. The owning adapter's - :attr:`NewtonActuatorAdapter.num_joints` is exactly this value. - device: Warp device string (e.g. ``"cuda:0"``). - joint_user_to_backend_indices: Complete permutation from public joint - indices to adapter-local joint indices. For Newton's global adapter, - adapter-local order is backend order. ``None`` preserves adapter-local order. - - Returns: - Tuple containing the following values: - - * ``stiffness``: Initial gains [N/m or N·m/rad, depending on joint - type], shape ``(num_envs, num_joints)``, dtype ``torch.float32``, on - :paramref:`device`. - * ``damping``: Initial gains [N·s/m or N·m·s/rad, depending on joint - type], shape ``(num_envs, num_joints)``, dtype ``torch.float32``, on - :paramref:`device`. - * ``joint_indices``: ``slice(None)`` when every joint is managed; - otherwise, a ``torch.int32`` tensor on :paramref:`device` containing - managed columns in the same adapter-local or public order as the gain tensors. - - Raises: - ValueError: If :paramref:`joint_user_to_backend_indices` is not a - complete permutation of all adapter-local joint indices. - """ - user_to_backend: tuple[int, ...] | None = None - if joint_user_to_backend_indices is not None: - user_to_backend = tuple(int(index) for index in joint_user_to_backend_indices) - if sorted(user_to_backend) != list(range(num_joints)): - raise ValueError( - "joint_user_to_backend_indices must contain each backend joint index exactly once; " - f"expected a permutation of 0..{num_joints - 1}, got {user_to_backend}." - ) - - arti_actuators = [act for act in actuators if dof_offset <= int(act.indices.numpy()[0]) < dof_offset + num_joints] - - managed_local: set[int] = set() - for act in arti_actuators: - per_act = act.indices.shape[0] // num_envs - for global_dof in act.indices.numpy()[:per_act]: - local = int(global_dof) - dof_offset - if 0 <= local < num_joints: - managed_local.add(local) - joint_indices: torch.Tensor | slice - if len(managed_local) == num_joints: - joint_indices = slice(None) - else: - joint_indices = torch.tensor(sorted(managed_local), dtype=torch.int32, device=device) - - wp_device = wp.get_device(device) - flat_stiffness = wp.zeros(num_envs * num_joints, dtype=wp.float32, device=wp_device) - flat_damping = wp.zeros(num_envs * num_joints, dtype=wp.float32, device=wp_device) - for act in arti_actuators: - ctrl = act.controller - if hasattr(ctrl, "kp"): - wp.launch( - scatter_gain_kernel, - dim=act.indices.shape[0], - inputs=[ctrl.kp, flat_stiffness, act.indices, dof_offset, num_joints, env_stride], - device=wp_device, - ) - if hasattr(ctrl, "kd"): - wp.launch( - scatter_gain_kernel, - dim=act.indices.shape[0], - inputs=[ctrl.kd, flat_damping, act.indices, dof_offset, num_joints, env_stride], - device=wp_device, - ) - stiffness = wp.to_torch(flat_stiffness.reshape((num_envs, num_joints))) - damping = wp.to_torch(flat_damping.reshape((num_envs, num_joints))) - if user_to_backend is not None: - # ``index_select(1, backend_column_indices)`` gathers backend-order columns into user-order - # positions: for each user position ``u`` it holds the backend column ``user_to_backend[u]``. - backend_column_indices = torch.tensor(user_to_backend, dtype=torch.long, device=device) - stiffness = stiffness.index_select(1, backend_column_indices) - damping = damping.index_select(1, backend_column_indices) - if not isinstance(joint_indices, slice): - backend_to_user = [0] * num_joints - for user_index, backend_index in enumerate(user_to_backend): - backend_to_user[backend_index] = user_index - joint_indices = torch.tensor( - sorted(backend_to_user[index] for index in managed_local), - dtype=torch.int32, - device=device, - ) - return stiffness, damping, joint_indices - - -# --------------------------------------------------------------------------- -# PhysX-only USD parsing -# --------------------------------------------------------------------------- - - -def _actuator_signature(parsed: Any) -> tuple: - """Build a hashable key from a parsed actuator spec for grouping. - - Joints whose prims resolve to the same signature share identical - controller type, gains, clamping chain, and delay configuration and - can therefore be merged into a single :class:`~newton.actuators.Actuator` - with combined index arrays. - """ - ctrl_resolved = parsed.controller_class.resolve_arguments( - dict(parsed.controller_kwargs), - ) - ctrl_key = (parsed.controller_class, tuple(sorted(ctrl_resolved.items()))) - - comp_keys: list[tuple] = [] - for comp_cls, comp_kwargs in parsed.component_specs: - resolved = comp_cls.resolve_arguments(comp_kwargs) - comp_keys.append((comp_cls, tuple(sorted(resolved.items())))) - comp_keys.sort(key=lambda t: t[0].__name__) - - return (ctrl_key, tuple(comp_keys)) - - -def _create_actuators_from_usd( - stage: Any, - joint_names: list[str], - num_envs: int, - num_total_joints: int, - device: str, - articulation_prim_path: str | None = None, -) -> list[Actuator]: - """Parse ``NewtonActuator`` prims and instantiate standalone actuators. - - This mirrors the actuator construction that Newton's - ``ModelBuilder.add_usd`` performs, but operates independently of a - Newton ``Model``. It is used on the PhysX backend where there is no - Newton simulation — actuators are stepped manually via the adapter. - - Because PhysX articulations have no free or ball joints, every - joint's coordinate count equals its DOF count. A single - ``indices`` array is therefore sufficient for all index roles - (``indices``, ``pos_indices``, ``target_pos_indices``). - - Joints with identical controller type, gains, clamping chain, and - delay are merged into one :class:`Actuator` with combined indices. - - Each per-DOF scalar parameter (``kp``, ``kd``, ``saturation_effort``, - etc.) is broadcast via :func:`wp.full` to match the group size. - Parameters marked as ``SHARED_PARAMS`` on the controller or clamping - class (e.g. ``model_path``, ``lookup_positions``) are passed through - directly without broadcast. - """ - from collections import defaultdict # noqa: PLC0415 - - from newton.actuators import parse_actuator_prim # noqa: PLC0415 - - from pxr import Usd # noqa: PLC0415 - - wp_device = wp.get_device(device) - - joint_name_to_idx: dict[str, int] = {name: i for i, name in enumerate(joint_names)} - - if articulation_prim_path is not None: - root_prim = stage.GetPrimAtPath(articulation_prim_path) - else: - root_prim = stage.GetPseudoRoot() - - parsed_per_joint: dict[int, Any] = {} - for prim in Usd.PrimRange(root_prim): - parsed = parse_actuator_prim(prim) - if parsed is None: - continue - target_name = parsed.target_path.rsplit("/", 1)[-1] - if target_name in joint_name_to_idx: - parsed_per_joint[joint_name_to_idx[target_name]] = parsed - - if not parsed_per_joint: - raise ValueError(f"No NewtonActuator prims found targeting any of: {joint_names}") - - groups: dict[tuple, list[int]] = defaultdict(list) - sig_to_parsed: dict[tuple, Any] = {} - for local_idx, parsed in sorted(parsed_per_joint.items()): - sig = _actuator_signature(parsed) - groups[sig].append(local_idx) - if sig not in sig_to_parsed: - sig_to_parsed[sig] = parsed - - actuators = [] - for sig, local_indices in groups.items(): - parsed = sig_to_parsed[sig] - - flat_indices = np.array( - [idx + e * num_total_joints for e in range(num_envs) for idx in local_indices], - dtype=np.uint32, - ) - indices = wp.array(flat_indices, device=wp_device) - num_dofs_in_group = len(local_indices) * num_envs - - # Controller - ctrl_kwargs = dict(parsed.controller_kwargs) - resolved = parsed.controller_class.resolve_arguments(ctrl_kwargs) - shared_ctrl = getattr(parsed.controller_class, "SHARED_PARAMS", set()) - ctrl_arrays = {} - for key, val in resolved.items(): - if key in shared_ctrl: - ctrl_arrays[key] = val - else: - ctrl_arrays[key] = wp.full(num_dofs_in_group, float(val), dtype=wp.float32, device=wp_device) - controller = parsed.controller_class(**ctrl_arrays) - - # Components (delay + clampings) - clampings = [] - delay = None - for comp_cls, comp_kwargs in parsed.component_specs: - if issubclass(comp_cls, Delay): - resolved_kw = Delay.resolve_arguments(comp_kwargs) - delay_steps = int(resolved_kw.get("delay_steps", 0)) - if delay_steps > 0: - delay_arr = wp.full(num_dofs_in_group, delay_steps, dtype=wp.int32, device=wp_device) - delay = Delay(delay_steps=delay_arr, max_delay=delay_steps) - elif issubclass(comp_cls, Clamping): - resolved_kw = comp_cls.resolve_arguments(comp_kwargs) - shared_clamp = getattr(comp_cls, "SHARED_PARAMS", set()) - clamp_arrays = {} - for k, v in resolved_kw.items(): - if k in shared_clamp: - clamp_arrays[k] = v - else: - clamp_arrays[k] = wp.full( - num_dofs_in_group, - float(v), - dtype=wp.float32, - device=wp_device, - ) - clampings.append(comp_cls(**clamp_arrays)) - - actuator = Actuator( - indices=indices, - controller=controller, - delay=delay, - clamping=clampings if clampings else None, - ) - actuators.append(actuator) - - return actuators diff --git a/source/isaaclab_newton/isaaclab_newton/assets/articulation/actuator_control.py b/source/isaaclab_newton/isaaclab_newton/assets/articulation/actuator_control.py new file mode 100644 index 000000000000..1d74b23059ff --- /dev/null +++ b/source/isaaclab_newton/isaaclab_newton/assets/articulation/actuator_control.py @@ -0,0 +1,195 @@ +# 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 actuator control adapter.""" + +from __future__ import annotations + +import logging +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import warp as wp + +from isaaclab.actuators import ActuatorCollection +from isaaclab.actuators.actuator_base_cfg import _is_implicit_actuator_cfg +from isaaclab.actuators.actuator_control import ArticulationActuatorControl +from isaaclab.actuators.newton import build_implicit_dof_mask +from isaaclab.actuators.newton import kernels as actuator_kernels +from isaaclab.actuators.newton.adapter import NewtonActuatorSelection +from isaaclab.assets.articulation import ordering_kernels +from isaaclab.sim.schemas.schemas_actuators import _validate_newton_native_actuator_cfgs + +from isaaclab_newton.physics import NewtonManager as SimulationManager + +if TYPE_CHECKING: + from .articulation import Articulation + +logger = logging.getLogger(__name__) + + +class NewtonActuatorControl(ArticulationActuatorControl): + """Actuator control adapter for the Newton backend.""" + + def __init__(self, articulation: Articulation): + """Initialize the control adapter. + + Args: + articulation: Newton articulation that owns backend simulation handles. + """ + super().__init__(articulation) + + def prepare_native_actuators(self, collection: ActuatorCollection, actuator_cfgs: dict) -> set[str]: + articulation = self._articulation + articulation._has_newton_actuators = False + articulation._implicit_dof_mask = None + articulation.newton_actuator_adapter = None + + if not getattr(articulation._sim_cfg, "use_newton_actuators", False): + return set() + + _validate_newton_native_actuator_cfgs(actuator_cfgs) + # Activate the Newton path even without explicit native groups: implicit-only + # articulations still rely on it for the solver telemetry fast path. + self._native_actuator_path_active = True + articulation._has_newton_actuators = True + SimulationManager.activate_newton_actuator_path() + + return {name for name, actuator_cfg in actuator_cfgs.items() if not _is_implicit_actuator_cfg(actuator_cfg)} + + def finalize_native_actuators(self, collection: ActuatorCollection) -> NewtonActuatorSelection | None: + if not self._native_actuator_path_active: + return None + + articulation = self._articulation + adapter = SimulationManager._adapter + if adapter is not None: + arti_start = self._joint_dof_offset() + binding = adapter.bind_articulation( + implicit_joint_indices=collection._implicit_group_joint_indices(), + dof_offset=arti_start, + num_joints=self.num_joints, + ) + articulation.newton_actuator_adapter = adapter + articulation._implicit_dof_mask = binding.implicit_dof_mask + articulation._implicit_dof_mask_owner = binding.implicit_dof_mask_owner + articulation._data._sim_bind_joint_computed_effort = binding.computed_effort_view + else: + articulation._implicit_dof_mask, articulation._implicit_dof_mask_owner = build_implicit_dof_mask( + collection._implicit_group_joint_indices(), + self.num_joints, + self.device, + ) + articulation._data._sim_bind_joint_computed_effort = wp.zeros( + (self.num_instances, self.num_joints), + dtype=wp.float32, + device=self.device, + ) + + def _post_actuator() -> None: + wp.launch( + actuator_kernels.sync_torque_telemetry, + dim=(self.num_instances, self.num_joints), + inputs=[ + articulation._data._sim_bind_joint_pos, + articulation._data._sim_bind_joint_vel, + collection._joint_pos_target, + collection._joint_vel_target, + articulation._data.joint_stiffness.warp, + articulation._data.joint_damping.warp, + articulation._data.joint_effort_limits.warp, + articulation._implicit_dof_mask, + articulation._data._sim_bind_joint_effort, + articulation._data._sim_bind_joint_computed_effort, + articulation._joint_user_to_backend_map(), + articulation.data.has_joint_ordering, + ], + outputs=[ + collection._computed_effort, + collection._applied_effort, + ], + device=self.device, + ) + + SimulationManager.register_post_actuator_callback(_post_actuator) + + if adapter is None: + return None + joint_ordering = articulation.data.joint_ordering + return NewtonActuatorSelection( + view=articulation._root_view, + actuators=adapter.actuators, + joint_user_to_backend_indices=( + joint_ordering.user_to_backend_indices if joint_ordering is not None else None + ), + ) + + def compute_native_actuators(self, collection: ActuatorCollection, dt: float) -> bool: + return self._native_actuator_path_active + + def submit_commands(self, collection: ActuatorCollection) -> None: + articulation = self._articulation + if self._native_actuator_path_active: + # Newton consumes raw explicit-actuator targets through joint_act. + user_effort = collection._joint_effort_target + user_pos_target = collection._joint_pos_target + user_vel_target = collection._joint_vel_target + write_pos_target = True + write_vel_target = True + write_joint_act = True + if not articulation.data.has_joint_ordering: + articulation.data._sim_bind_joint_position_target.assign(collection._joint_pos_target) + articulation.data._sim_bind_joint_velocity_target.assign(collection._joint_vel_target) + articulation.data._sim_bind_joint_act.assign(collection._joint_effort_target) + articulation.data._sim_bind_joint_effort.assign(collection._joint_effort_target) + return + else: + # Lab executors publish processed targets; only implicit joints use + # the backend position and velocity drives. + user_effort = collection._joint_effort_target_sim + user_pos_target = collection._joint_pos_target_sim + user_vel_target = collection._joint_vel_target_sim + write_pos_target = collection.has_implicit_actuators + write_vel_target = collection.has_implicit_actuators + write_joint_act = False + if not articulation.data.has_joint_ordering: + articulation.data._sim_bind_joint_effort.assign(collection._joint_effort_target_sim) + if collection.has_implicit_actuators: + articulation.data._sim_bind_joint_position_target.assign(collection._joint_pos_target_sim) + articulation.data._sim_bind_joint_velocity_target.assign(collection._joint_vel_target_sim) + return + + ordering_kernels.launch_reorder_joint_targets_user_to_backend( + user_effort=user_effort, + user_pos_target=user_pos_target, + user_vel_target=user_vel_target, + backend_to_user=articulation._joint_backend_to_user_map(), + write_effort=True, + write_pos_target=write_pos_target, + write_vel_target=write_vel_target, + write_joint_act=write_joint_act, + backend_effort=articulation.data._sim_bind_joint_effort, + backend_pos_target=articulation.data._sim_bind_joint_position_target, + backend_vel_target=articulation.data._sim_bind_joint_velocity_target, + backend_joint_act=articulation.data._sim_bind_joint_act, + device=self.device, + ) + + def reset_native_actuators(self, env_ids: Sequence[int] | slice) -> None: + if self._native_actuator_path_active and SimulationManager._adapter is not None: + SimulationManager._adapter.reset(env_ids) + + def _joint_dof_offset(self) -> int: + """Return the first selected joint DOF's model offset within an environment.""" + from newton import Model as NewtonModel # noqa: PLC0415 + + dof_layout = self._articulation._root_view.frequency_layouts[NewtonModel.AttributeFrequency.JOINT_DOF] + if dof_layout.slice is not None: + selection_offset = dof_layout.slice.start + elif dof_layout.indices is not None: + selection_offset = int(dof_layout.indices.numpy()[0]) + else: + selection_offset = 0 + return dof_layout.offset + selection_offset diff --git a/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py b/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py index 0a55cfab2c68..cd56ee56f1bf 100644 --- a/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py +++ b/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py @@ -8,7 +8,6 @@ from __future__ import annotations -import importlib.util import logging import warnings from collections.abc import Sequence @@ -23,16 +22,13 @@ from pxr import UsdPhysics -from isaaclab.actuators import ActuatorBase, ActuatorBaseCfg, ImplicitActuator +from isaaclab.actuators import ActuatorCollection +from isaaclab.actuators.actuator_base_cfg import _is_implicit_actuator_cfg from isaaclab.assets.articulation import ordering_kernels from isaaclab.assets.articulation.base_articulation import BaseArticulation -from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source - -_HAS_NEWTON_ACTUATORS = importlib.util.find_spec("isaaclab_newton.actuators") is not None - from isaaclab.physics import PhysicsEvent +from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source from isaaclab.utils.string import resolve_matching_names, resolve_matching_names_values -from isaaclab.utils.types import ArticulationActions from isaaclab.utils.version import get_isaac_sim_version, has_kit from isaaclab.utils.warp import ProxyArray from isaaclab.utils.wrench_composer import WrenchComposer @@ -41,6 +37,7 @@ from isaaclab_newton.assets.articulation import kernels as articulation_kernels from isaaclab_newton.physics import NewtonManager as SimulationManager +from .actuator_control import NewtonActuatorControl from .articulation_data import ArticulationData if TYPE_CHECKING: @@ -51,12 +48,6 @@ logger = logging.getLogger(__name__) -def _is_implicit_actuator_cfg(actuator_cfg: ActuatorBaseCfg) -> bool: - """Return whether an actuator config constructs an implicit actuator without instantiating it.""" - class_type = actuator_cfg.class_type - return "ImplicitActuator" in class_type if isinstance(class_type, str) else issubclass(class_type, ImplicitActuator) - - def _target_mode_from_gains(stiffness: float, damping: float) -> JointTargetMode: """Infer the Newton target mode for an implicit actuator's effective gains.""" if stiffness != 0.0 and damping != 0.0: @@ -367,20 +358,12 @@ def reset(self, env_ids: Sequence[int] | None = None, env_mask: wp.array | None env_ids: Environment indices. If None, then all indices are used. env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). """ - if isinstance(env_ids, slice) and env_ids == slice(None): - env_ids = None - # reset Lab actuators registered on this articulation - for actuator in self.actuators.values(): - actuator.reset(env_ids) - # reset the global Newton actuator adapter (its ``_states_a/_b`` buffers - # carry per-env state — delay queues, neural hidden states — that must - # be cleared for the resetting envs). The adapter spans the whole model, - # so calling reset here resets state for every articulation that shares - # this env id; that's correct because env ids are world-scoped. - # ``getattr`` guards subclasses (e.g. ``Multirotor``) that override - # ``_process_actuators_cfg`` and never initialize ``_has_newton_actuators``. - if getattr(self, "_has_newton_actuators", False) and SimulationManager._adapter is not None: - SimulationManager._adapter.reset(env_ids) + # use ellipses object to skip initial indices. + if (env_ids is None) or (env_ids == slice(None)): + env_ids = slice(None) + # reset actuators, including backend-native actuator state. None selects all + # environments; delayed-actuator buffers do not accept a slice. + self.actuators.reset(None if env_ids == slice(None) else env_ids) # reset external wrenches. self._instantaneous_wrench_composer.reset(env_ids, env_mask) self._permanent_wrench_composer.reset(env_ids, env_mask) @@ -405,14 +388,9 @@ def write_data_to_sim(self): composer.compose_to_body_frame() # Kept separate from the joint-target gather below: this scatter runs # over bodies while the target gather runs over joints (mismatched - # item axes), and it must precede ``_apply_actuator_model``, which - # produces the target inputs. A merged kernel would need a divergent - # max-dim launch and would break that ordering, so there is no win. - # - # The body-ordering decision is branched here in Python rather than - # carried into the step kernel: identity ordering writes straight into - # the sim-bound wrench with no body remap (the reorder-free data path), - # while non-identity ordering scatters through the body map. + # item axes), and it must precede the actuator compute/submit below, + # which produces the target inputs. A merged kernel would need a + # divergent max-dim launch and would break that ordering, so there is no win. if self.data.has_body_ordering: wp.launch( articulation_kernels.update_wrench_array_with_force_and_torque_ordered, @@ -445,88 +423,10 @@ def write_data_to_sim(self): if self._instantaneous_wrench_composer.active: self._instantaneous_wrench_composer.reset() - if getattr(self, "_has_newton_actuators", False): - # Raw targets go directly to Newton's control object. Newton PD - # consumes ``joint_act`` for explicit (Newton-managed) joints; the - # solver's built-in joint drive does the PD for implicit joints - # (whose stiffness/damping are non-zero in sim) and adds whatever - # is in ``joint_f`` as feedforward. We pre-fill ``joint_f`` with - # the user's effort target across all DOFs here; the adapter step - # will zero it at explicit DOFs and overwrite them with each - # actuator's computed effort, while implicit DOFs keep the FF. - # Identity ordering copies the four targets straight into the - # sim-bound buffers: the asset joint map is then an identity arange, - # so the fused gather would be a pure identity permutation. Branching - # here keeps the no-reorder case at zero launch overhead, matching the - # pre-ordering data path. Non-identity ordering fuses the four - # per-buffer reorders into one gather; the effort source is read once - # and feeds both joint_act and joint_effort. - if not self.data.has_joint_ordering: - self.data._sim_bind_joint_position_target.assign(self._data._joint_pos_target) - self.data._sim_bind_joint_velocity_target.assign(self._data._joint_vel_target) - self.data._sim_bind_joint_act.assign(self._data._joint_effort_target) - self.data._sim_bind_joint_effort.assign(self._data._joint_effort_target) - else: - wp.launch( - ordering_kernels.reorder_joint_targets_user_to_backend, - dim=(self.num_instances, self.num_joints), - inputs=[ - self._data._joint_effort_target, - self._data._joint_pos_target, - self._data._joint_vel_target, - self._joint_backend_to_user_map(), - True, - True, - True, - True, - ], - outputs=[ - self.data._sim_bind_joint_effort, - self.data._sim_bind_joint_position_target, - self.data._sim_bind_joint_velocity_target, - self.data._sim_bind_joint_act, - ], - device=self.device, - ) - else: - # Standard Lab actuator path - self._apply_actuator_model() - # Identity ordering copies the computed targets straight into the - # sim-bound buffers; position/velocity are only forwarded when an - # implicit actuator consumes them. Branching here keeps the no-reorder - # case at zero launch overhead, matching the pre-ordering data path. - # Non-identity ordering fuses the effort reorder with the optional - # position/velocity reorders. The last gather output targets the - # dedicated joint-act buffer (unused here: write_joint_act is off, so - # it is never written) rather than aliasing the effort buffer, keeping - # the launch's output dependencies distinct for graph capture. - if not self.data.has_joint_ordering: - self.data._sim_bind_joint_effort.assign(self._joint_effort_target_sim) - if self._has_implicit_actuators: - self.data._sim_bind_joint_position_target.assign(self._joint_pos_target_sim) - self.data._sim_bind_joint_velocity_target.assign(self._joint_vel_target_sim) - else: - wp.launch( - ordering_kernels.reorder_joint_targets_user_to_backend, - dim=(self.num_instances, self.num_joints), - inputs=[ - self._joint_effort_target_sim, - self._joint_pos_target_sim, - self._joint_vel_target_sim, - self._joint_backend_to_user_map(), - True, - self._has_implicit_actuators, - self._has_implicit_actuators, - False, - ], - outputs=[ - self.data._sim_bind_joint_effort, - self.data._sim_bind_joint_position_target, - self.data._sim_bind_joint_velocity_target, - self.data._sim_bind_joint_act, - ], - device=self.device, - ) + # Compute processed actuator commands (native path is a no-op here) and + # submit them to the backend through the collection's control adapter. + self.actuators.compute(SimulationManager.get_physics_dt()) + self.actuators.submit_commands() def update(self, dt: float): """Updates the simulation data. @@ -1743,6 +1643,50 @@ def write_joint_damping_to_sim_index( env_ids=env_ids, ) + def write_actuator_stiffness_to_sim( + self, + *, + stiffness: torch.Tensor, + env_ids: torch.Tensor, + joint_ids: torch.Tensor, + ) -> None: + """Write native actuator stiffness [N/m or N·m/rad, depending on joint type]. + + .. deprecated:: 3.0 + Use :func:`isaaclab.envs.mdp.events.randomize_actuator_gains` for + managed randomization. Direct controller-gain writes have no public + replacement. This method will be removed in 4.0. + + Args: + stiffness: Controller stiffness [N/m or N·m/rad, depending on joint type]. + env_ids: Articulation instance indices. + joint_ids: Articulation-local joint indices. + """ + self._write_deprecated_native_actuator_gain( + "write_actuator_stiffness_to_sim", "kp", stiffness, env_ids, joint_ids + ) + + def write_actuator_damping_to_sim( + self, + *, + damping: torch.Tensor, + env_ids: torch.Tensor, + joint_ids: torch.Tensor, + ) -> None: + """Write native actuator damping [N·s/m or N·m·s/rad, depending on joint type]. + + .. deprecated:: 3.0 + Use :func:`isaaclab.envs.mdp.events.randomize_actuator_gains` for + managed randomization. Direct controller-gain writes have no public + replacement. This method will be removed in 4.0. + + Args: + damping: Controller damping [N·s/m or N·m·s/rad, depending on joint type]. + env_ids: Articulation instance indices. + joint_ids: Articulation-local joint indices. + """ + self._write_deprecated_native_actuator_gain("write_actuator_damping_to_sim", "kd", damping, env_ids, joint_ids) + def write_joint_damping_to_sim_mask( self, *, @@ -1773,93 +1717,6 @@ def write_joint_damping_to_sim_mask( env_mask=env_mask, ) - def write_actuator_stiffness_to_sim( - self, - *, - stiffness: torch.Tensor, - env_ids: torch.Tensor, - joint_ids: torch.Tensor, - ) -> None: - """Write actuator kp at the (env_ids, joint_ids) sub-grid and propagate to controllers. - - Iterates the global adapter's Newton actuators and uses - :meth:`ArticulationView.get_actuator_parameter` / - :meth:`~ArticulationView.set_actuator_parameter` to patch each - controller's ``kp`` array. Actuators belonging to a different - articulation are no-ops because the view's per-DOF mapping - returns ``-1`` for DOFs outside this articulation's range. - - Args: - stiffness: Sub-grid of new kp values, shape ``(len(env_ids), len(joint_ids))``. - env_ids: 1D torch tensor of env indices. - joint_ids: 1D torch tensor of articulation-local joint indices. - - No-op when the Newton fast path is not active. - """ - self._write_actuator_param("kp", stiffness, env_ids, joint_ids) - - def write_actuator_damping_to_sim( - self, - *, - damping: torch.Tensor, - env_ids: torch.Tensor, - joint_ids: torch.Tensor, - ) -> None: - """Write actuator kd at the (env_ids, joint_ids) sub-grid and propagate to controllers.""" - self._write_actuator_param("kd", damping, env_ids, joint_ids) - - def _write_actuator_param( - self, - attr: str, - values: torch.Tensor, - env_ids: torch.Tensor, - joint_ids: torch.Tensor, - ) -> None: - """Shared body for :meth:`write_actuator_stiffness_to_sim` / :meth:`write_actuator_damping_to_sim`.""" - # TODO: This routes through per-actuator torch indexing and has no mask - # variant because the actuator gain buffers are per-actuator torch views - # over arbitrary joint-index subsets. A single-launch warp path and a mask - # variant need the actuator-side buffer layout rework, deferred to the - # actuator rework built on this series. - from isaaclab_newton.actuators import kernels as actuator_kernels # noqa: PLC0415 - - adapter = self.newton_actuator_adapter - if adapter is None: - return - - env_ids_wp = wp.from_torch( - env_ids.to(self.device, dtype=torch.int32).contiguous(), - dtype=wp.int32, - ) - env_mask = wp.zeros(self.num_instances, dtype=wp.bool, device=self.device) - wp.launch( - actuator_kernels.set_mask_kernel, - dim=env_ids_wp.shape[0], - inputs=[env_mask, env_ids_wp], - device=self.device, - ) - - env_ids_long = env_ids.to(self.device, dtype=torch.long).unsqueeze(1) - joint_ids_backend = joint_ids.to(self.device, dtype=torch.long) - if self.data.has_joint_ordering: - joint_ids_backend = self._joint_user_to_backend_torch[joint_ids_backend] - joint_ids_backend = joint_ids_backend.unsqueeze(0) - - for act in adapter.actuators: - ctrl = act.controller - if not hasattr(ctrl, attr): - continue - cur_wp = self._root_view.get_actuator_parameter(act, ctrl, attr) - cur_torch = wp.to_torch(cur_wp) - cur_torch[env_ids_long, joint_ids_backend] = values.to(cur_torch.device, dtype=cur_torch.dtype) - self._root_view.set_actuator_parameter( - actuator=act, - component=ctrl, - name=attr, - values=cur_wp, - mask=env_mask, - ) - def write_joint_position_limit_to_sim_index( self, *, @@ -2212,6 +2069,8 @@ def write_joint_friction_coefficient_to_sim_index( self, *, joint_friction_coeff: torch.Tensor | wp.array | float, + joint_dynamic_friction_coeff: torch.Tensor | wp.array | float | None = None, + joint_viscous_friction_coeff: torch.Tensor | wp.array | float | None = None, joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, ): @@ -2239,9 +2098,28 @@ def write_joint_friction_coefficient_to_sim_index( Args: joint_friction_coeff: Joint friction force/torque [N or N·m, depending on joint type]. Shape is (len(env_ids), len(joint_ids)). + joint_dynamic_friction_coeff: Dynamic friction values. Newton has no dynamic joint + friction property; nonzero values are ignored with a warning. + joint_viscous_friction_coeff: Viscous friction values [N·s/m or N·m·s/rad, depending on + joint type] with the same shape. If None, the viscous component is not updated. joint_ids: Joint indices. If None, then all joints are used. env_ids: Environment indices. If None, then all indices are used. """ + if joint_dynamic_friction_coeff is not None: + dynamic = joint_dynamic_friction_coeff + if isinstance(dynamic, wp.array): + dynamic = wp.to_torch(dynamic) + has_dynamic = dynamic != 0.0 if isinstance(dynamic, (float, int)) else bool(torch.any(dynamic != 0.0)) + if has_dynamic: + logger.warning( + "Newton has no dynamic joint friction property; ignoring nonzero 'joint_dynamic_friction_coeff'." + ) + if joint_viscous_friction_coeff is not None: + self.write_joint_viscous_friction_coefficient_to_sim_index( + joint_viscous_friction_coeff=joint_viscous_friction_coeff, + joint_ids=joint_ids, + env_ids=env_ids, + ) self._write_joint_float_property_to_sim_index( joint_friction_coeff, value_name="joint_friction_coeff", @@ -2662,256 +2540,6 @@ def set_inertias_mask( # tell the physics engine that some of the body properties have been updated SimulationManager.add_model_change(ModelFlags.BODY_INERTIAL_PROPERTIES) - def set_joint_position_target_index( - self, - *, - target: torch.Tensor | wp.array, - joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - ) -> None: - """Set joint position targets into internal buffers using indices. - - This function does not apply the joint targets to the simulation. It only fills the buffers with - the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. - - .. note:: - This method expects partial data. - - .. tip:: - Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. - However, to allow graphed pipelines, the mask method must be used. - - Args: - target: Joint position targets. Shape is (len(env_ids), len(joint_ids)). - joint_ids: The joint indices to set the targets for. Defaults to None (all joints). - env_ids: The environment indices to set the targets for. Defaults to None (all environments). - """ - # resolve all indices - env_ids = self._resolve_env_ids(env_ids) - joint_ids = self._resolve_joint_ids(joint_ids) - self.assert_shape_and_dtype(target, (env_ids.shape[0], joint_ids.shape[0]), wp.float32, "target") - # Warp kernels can ingest torch tensors directly, so we don't need to convert to warp arrays here. - wp.launch( - shared_kernels.write_2d_data_to_buffer_with_indices_kernel(env_ids, joint_ids), - dim=(env_ids.shape[0], joint_ids.shape[0]), - inputs=[ - target, - env_ids, - joint_ids, - ], - outputs=[ - self.data._joint_pos_target, - ], - device=self.device, - ) - # Only updates internal buffers, does not apply the targets to the simulation. - - def set_joint_position_target_mask( - self, - *, - target: torch.Tensor | wp.array, - joint_mask: wp.array | None = None, - env_mask: wp.array | None = None, - ) -> None: - """Set joint position targets into internal buffers using masks. - - .. note:: - This method expects full data. - - .. tip:: - Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. - However, to allow graphed pipelines, the mask method must be used. - - Args: - target: Joint position targets. Shape is (num_instances, num_joints). - joint_mask: Joint mask. If None, then all joints are used. Shape is (num_joints,). - env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). - """ - env_mask = self._resolve_mask(env_mask, self._ALL_ENV_MASK) - joint_mask = self._resolve_mask(joint_mask, self._ALL_JOINT_MASK) - self.assert_shape_and_dtype_mask(target, (env_mask, joint_mask), wp.float32, "target") - wp.launch( - shared_kernels.write_2d_data_to_buffer_with_mask, - dim=(env_mask.shape[0], joint_mask.shape[0]), - inputs=[ - target, - env_mask, - joint_mask, - ], - outputs=[ - self.data._joint_pos_target, - ], - device=self.device, - ) - # Only updates internal buffers, does not apply the targets to the simulation. - - def set_joint_velocity_target_index( - self, - *, - target: torch.Tensor | wp.array, - joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - ) -> None: - """Set joint velocity targets into internal buffers using indices. - - This function does not apply the joint targets to the simulation. It only fills the buffers with - the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. - - .. note:: - This method expects partial data. - - .. tip:: - Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. - However, to allow graphed pipelines, the mask method must be used. - - Args: - target: Joint velocity targets. Shape is (len(env_ids), len(joint_ids)). - joint_ids: The joint indices to set the targets for. Defaults to None (all joints). - env_ids: The environment indices to set the targets for. Defaults to None (all environments). - """ - # resolve all indices - env_ids = self._resolve_env_ids(env_ids) - joint_ids = self._resolve_joint_ids(joint_ids) - self.assert_shape_and_dtype(target, (env_ids.shape[0], joint_ids.shape[0]), wp.float32, "target") - # Warp kernels can ingest torch tensors directly, so we don't need to convert to warp arrays here. - wp.launch( - shared_kernels.write_2d_data_to_buffer_with_indices_kernel(env_ids, joint_ids), - dim=(env_ids.shape[0], joint_ids.shape[0]), - inputs=[ - target, - env_ids, - joint_ids, - ], - outputs=[ - self.data._joint_vel_target, - ], - device=self.device, - ) - # Only updates internal buffers, does not apply the targets to the simulation. - - def set_joint_velocity_target_mask( - self, - *, - target: torch.Tensor | wp.array, - joint_mask: wp.array | None = None, - env_mask: wp.array | None = None, - ) -> None: - """Set joint velocity targets into internal buffers using masks. - - .. note:: - This method expects full data. - - .. tip:: - Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. - However, to allow graphed pipelines, the mask method must be used. - - Args: - target: Joint velocity targets. Shape is (num_instances, num_joints). - joint_mask: Joint mask. If None, then all joints are used. Shape is (num_joints,). - env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). - """ - # Resolve masks. - env_mask = self._resolve_mask(env_mask, self._ALL_ENV_MASK) - joint_mask = self._resolve_mask(joint_mask, self._ALL_JOINT_MASK) - self.assert_shape_and_dtype_mask(target, (env_mask, joint_mask), wp.float32, "target") - wp.launch( - shared_kernels.write_2d_data_to_buffer_with_mask, - dim=(env_mask.shape[0], joint_mask.shape[0]), - inputs=[ - target, - env_mask, - joint_mask, - ], - outputs=[ - self.data._joint_vel_target, - ], - device=self.device, - ) - # Only updates internal buffers, does not apply the targets to the simulation. - - def set_joint_effort_target_index( - self, - *, - target: torch.Tensor | wp.array, - joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - ) -> None: - """Set joint efforts into internal buffers using indices. - - This function does not apply the joint targets to the simulation. It only fills the buffers with - the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. - - .. note:: - This method expects partial data. - - .. tip:: - Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. - However, to allow graphed pipelines, the mask method must be used. - - Args: - target: Joint effort targets. Shape is (len(env_ids), len(joint_ids)). - joint_ids: The joint indices to set the targets for. Defaults to None (all joints). - env_ids: The environment indices to set the targets for. Defaults to None (all environments). - """ - # resolve all indices - env_ids = self._resolve_env_ids(env_ids) - joint_ids = self._resolve_joint_ids(joint_ids) - self.assert_shape_and_dtype(target, (env_ids.shape[0], joint_ids.shape[0]), wp.float32, "target") - # Warp kernels can ingest torch tensors directly, so we don't need to convert to warp arrays here. - wp.launch( - shared_kernels.write_2d_data_to_buffer_with_indices_kernel(env_ids, joint_ids), - dim=(env_ids.shape[0], joint_ids.shape[0]), - inputs=[ - target, - env_ids, - joint_ids, - ], - outputs=[ - self.data._joint_effort_target, - ], - device=self.device, - ) - # Only updates internal buffers, does not apply the targets to the simulation. - - def set_joint_effort_target_mask( - self, - *, - target: torch.Tensor | wp.array, - joint_mask: wp.array | None = None, - env_mask: wp.array | None = None, - ) -> None: - """Set joint efforts into internal buffers using masks. - - .. note:: - This method expects full data. - - .. tip:: - Both the index and mask methods have dedicated optimized implementations. Performance is similar for both. - However, to allow graphed pipelines, the mask method must be used. - - Args: - target: Joint effort targets. Shape is (num_instances, num_joints). - joint_mask: Joint mask. If None, then all joints are used. Shape is (num_joints,). - env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). - """ - env_mask = self._resolve_mask(env_mask, self._ALL_ENV_MASK) - joint_mask = self._resolve_mask(joint_mask, self._ALL_JOINT_MASK) - self.assert_shape_and_dtype_mask(target, (env_mask, joint_mask), wp.float32, "target") - wp.launch( - shared_kernels.write_2d_data_to_buffer_with_mask, - dim=(env_mask.shape[0], joint_mask.shape[0]), - inputs=[ - target, - env_mask, - joint_mask, - ], - outputs=[ - self.data._joint_effort_target, - ], - device=self.device, - ) - # Only updates internal buffers, does not apply the targets to the simulation. - """ Operations - Tendons. """ @@ -3750,11 +3378,6 @@ def _create_buffers(self): SimulationManager.register_post_step_callback(self._post_step_callback) # tendon names are set in _process_tendons function - # -- joint commands (sent to the simulation after actuator processing) - self._joint_pos_target_sim = wp.zeros_like(self.data.joint_pos_target.warp, device=self.device) - self._joint_vel_target_sim = wp.zeros_like(self.data.joint_pos_target.warp, device=self.device) - self._joint_effort_target_sim = wp.zeros_like(self.data.joint_pos_target.warp, device=self.device) - # soft joint position limits (recommended not to be too close to limits). wp.launch( articulation_kernels.update_soft_joint_pos_limits, @@ -3824,315 +3447,16 @@ def _invalidate_initialize_callback(self, event): """ def _process_actuators_cfg(self): - """Process and apply articulation joint properties.""" - # create actuators - self.actuators = dict() - # flag for implicit actuators - # if this is false, we by-pass certain checks when doing actuator-related operations - self._has_implicit_actuators = False - self._has_newton_actuators = False - # Per-DOF implicit/explicit mask consumed by the in-graph kernel - # ``sync_torque_telemetry``. ``None`` when no Newton fast path is active. - self._implicit_dof_mask: wp.array | None = None - # Reference to the global Newton actuator adapter (or ``None`` - # when this articulation has no explicit Newton actuators) and a - # per-articulation kp/kd snapshot consumed by - # ``randomize_actuator_gains`` to seed its DR baselines. - self.newton_actuator_adapter = None - self.newton_default_stiffness: torch.Tensor | None = None - self.newton_default_damping: torch.Tensor | None = None - self.newton_managed_local_joints: torch.Tensor | slice | None = None - - _use_newton_actuators = getattr(self._sim_cfg, "use_newton_actuators", False) - - if _use_newton_actuators and not _HAS_NEWTON_ACTUATORS: - logger.warning( - "use_newton_actuators is enabled but 'newton.actuators' is not available. " - "Newton-native actuators will be disabled. Upgrade Newton to >= 1.2.0rc1." - ) - - if _use_newton_actuators and _HAS_NEWTON_ACTUATORS: - from newton import Model as NewtonModel # noqa: PLC0415 - - from isaaclab_newton.actuators import build_implicit_dof_mask # noqa: PLC0415 - from isaaclab_newton.actuators import kernels as actuator_kernels # noqa: PLC0415 - - # Enable the fast path even for all-implicit articulations: - # the solver runs PD internally; Lab only forwards targets. - self._has_newton_actuators = True - # Opt this articulation into the Newton fast path and (idempotently) - # build the single sim-level actuator adapter from ``model.actuators``. - SimulationManager.activate_newton_actuator_path() - - # Zero the simulator's joint-drive PD on DOFs covered by an explicit - # Lab actuator config in *this* articulation. The global Newton - # adapter's actuator step writes their effort to ``joint_f`` - # directly; the joint drive shouldn't add its own PD on top. - explicit_joint_ids: list[int] = [] - for actuator_cfg in self.cfg.actuators.values(): - cls_type = actuator_cfg.class_type - if ( - "ImplicitActuator" in cls_type - if isinstance(cls_type, str) - else issubclass(cls_type, ImplicitActuator) - ): - continue - joint_ids, _ = self.find_joints(actuator_cfg.joint_names_expr) - explicit_joint_ids.extend(int(j) for j in joint_ids) - if explicit_joint_ids: - explicit_ids_t = torch.tensor( - sorted(set(explicit_joint_ids)), - dtype=torch.int32, - device=self.device, - ) - self.write_joint_stiffness_to_sim_index(stiffness=0.0, joint_ids=explicit_ids_t) - self.write_joint_damping_to_sim_index(damping=0.0, joint_ids=explicit_ids_t) - - for actuator_name, actuator_cfg in self.cfg.actuators.items(): - cls_type = actuator_cfg.class_type - is_implicit = ( - "ImplicitActuator" in cls_type - if isinstance(cls_type, str) - else issubclass(cls_type, ImplicitActuator) - ) - if is_implicit: - self._create_lab_actuator(actuator_name, actuator_cfg) - else: - self._create_lab_actuator(actuator_name, actuator_cfg, properties_only=True) - - # Run the implicit-DOF FF-routing + telemetry kernel inside the - # captured graph, right after the actuator step. Closure captures - # the buffers we need via ``self._data``. - - # Bind this articulation to the global adapter (already built by - # ``activate_newton_actuator_path``): one call snapshots the initial - # gains, builds the implicit-DOF mask, and slices the adapter's - # computed-effort buffer to this articulation's columns. - # ``_implicit_dof_mask_owner`` is retained as an instance attribute - # so the torch tensor backing ``_implicit_dof_mask`` isn't freed - # while a captured CUDA graph holds a pointer into it. Falls back to - # a zero computed-effort buffer for all-implicit scenes where no - # global adapter exists — the kernel only reads it on explicit DOFs. - adapter = SimulationManager._adapter - if adapter is not None: - dof_layout = self._root_view.frequency_layouts[NewtonModel.AttributeFrequency.JOINT_DOF] - if dof_layout.slice is not None: - arti_start = dof_layout.slice.start - elif dof_layout.indices is not None: - arti_start = int(dof_layout.indices.numpy()[0]) - else: - arti_start = 0 - joint_ordering = self.joint_ordering - binding = adapter.bind_articulation( - lab_actuators=self.actuators, - dof_offset=arti_start, - num_joints=self.num_joints, - joint_user_to_backend_indices=( - joint_ordering.user_to_backend_indices if joint_ordering is not None else None - ), - ) - self.newton_actuator_adapter = adapter - self.newton_default_stiffness = binding.stiffness - self.newton_default_damping = binding.damping - self.newton_managed_local_joints = binding.joint_indices - self._implicit_dof_mask = binding.implicit_dof_mask - self._implicit_dof_mask_owner = binding.implicit_dof_mask_owner - self._data._sim_bind_joint_computed_effort = binding.computed_effort_view - else: - self._implicit_dof_mask, self._implicit_dof_mask_owner = build_implicit_dof_mask( - self.actuators, - self.num_joints, - self.device, - ) - self._data._sim_bind_joint_computed_effort = wp.zeros( - (self.num_instances, self.num_joints), - dtype=wp.float32, - device=self.device, - ) - - def _post_actuator() -> None: - wp.launch( - actuator_kernels.sync_torque_telemetry, - dim=(self.num_instances, self.num_joints), - inputs=[ - self._data._sim_bind_joint_pos, - self._data._sim_bind_joint_vel, - self._data._joint_pos_target, - self._data._joint_vel_target, - self._data.joint_stiffness.warp, - self._data.joint_damping.warp, - self._data.joint_effort_limits.warp, - self._implicit_dof_mask, - self._data._sim_bind_joint_effort, - self._data._sim_bind_joint_computed_effort, - self._joint_user_to_backend_map(), - self.data.has_joint_ordering, - ], - outputs=[ - self._data._computed_torque, - self._data._applied_torque, - ], - device=self.device, - ) - - SimulationManager.register_post_actuator_callback(_post_actuator) - - return - - # --- Standard Isaac Lab actuator path --- - for actuator_name, actuator_cfg in self.cfg.actuators.items(): - self._create_lab_actuator(actuator_name, actuator_cfg) - - # perform some sanity checks to ensure actuators are prepared correctly - total_act_joints = sum(actuator.num_joints for actuator in self.actuators.values()) - if total_act_joints != (self.num_joints - self.num_fixed_tendons): - logger.warning( - "Not all actuators are configured! Total number of actuated joints not equal to number of" - f" joints available: {total_act_joints} != {self.num_joints - self.num_fixed_tendons}." - ) - - if self.cfg.actuator_value_resolution_debug_print: - if _HAS_NEWTON_ACTUATORS: - from isaaclab_newton.actuators import NewtonActuatorAdapter # noqa: PLC0415 - else: - NewtonActuatorAdapter = None # type: ignore[assignment] - t = PrettyTable(["Group", "Property", "Name", "ID", "USD Value", "ActutatorCfg Value", "Applied"]) - for actuator_group, actuator in self.actuators.items(): - if NewtonActuatorAdapter is not None and isinstance(actuator, NewtonActuatorAdapter): - continue - group_count = 0 - for property, resolution_details in actuator.joint_property_resolution_table.items(): - for prop_idx, resolution_detail in enumerate(resolution_details): - actuator_group_str = actuator_group if group_count == 0 else "" - property_str = property if prop_idx == 0 else "" - fmt = [f"{v:.2e}" if isinstance(v, float) else str(v) for v in resolution_detail] - t.add_row([actuator_group_str, property_str, *fmt]) - group_count += 1 - logger.warning(f"\nActuatorCfg-USD Value Discrepancy Resolution (matching values are skipped): \n{t}") - - def _create_lab_actuator( - self, - actuator_name: str, - actuator_cfg: ActuatorBaseCfg, - *, - properties_only: bool = False, - ) -> None: - """Instantiate a single Lab actuator from its config and write properties to sim. - - Args: - actuator_name: Name for the actuator group. - actuator_cfg: Configuration for the actuator. - properties_only: When ``True``, only write physical joint properties - (armature, limits, friction) without registering the actuator or - writing stiffness/damping. Used for explicit joints managed by - Newton actuators. - """ - joint_ids, joint_names = self.find_joints(actuator_cfg.joint_names_expr, as_proxy=True) - if len(joint_names) == 0: - raise ValueError( - f"No joints found for actuator group: {actuator_name} with joint name expression:" - f" {actuator_cfg.joint_names_expr}." - ) - joint_ids = slice(None) if joint_names == self.joint_names else joint_ids.torch - torch_joint_ids = joint_ids - - actuator: ActuatorBase = actuator_cfg.class_type( - cfg=actuator_cfg, - joint_names=joint_names, - joint_ids=joint_ids, - num_envs=self.num_instances, - device=self.device, - stiffness=wp.to_torch(self._data.joint_stiffness)[:, torch_joint_ids], - damping=wp.to_torch(self._data.joint_damping)[:, torch_joint_ids], - armature=wp.to_torch(self._data.joint_armature)[:, torch_joint_ids], - friction=wp.to_torch(self._data.joint_friction_coeff)[:, torch_joint_ids], - viscous_friction=wp.to_torch(self._data.joint_viscous_friction_coeff)[:, torch_joint_ids], - effort_limit=wp.to_torch(self._data.joint_effort_limits)[:, torch_joint_ids].clone(), - velocity_limit=wp.to_torch(self._data.joint_vel_limits)[:, torch_joint_ids], - ) - - # Write physical joint properties (armature, limits, friction, passive damping) — always needed. - self.write_joint_effort_limit_to_sim_index( - limits=actuator.effort_limit_sim, - joint_ids=actuator.joint_indices, - ) - self.write_joint_velocity_limit_to_sim_index( - limits=actuator.velocity_limit_sim, - joint_ids=actuator.joint_indices, - ) - self.write_joint_armature_to_sim_index(armature=actuator.armature, joint_ids=actuator.joint_indices) - self.write_joint_friction_coefficient_to_sim_index( - joint_friction_coeff=actuator.friction, - joint_ids=actuator.joint_indices, - ) - self.write_joint_viscous_friction_coefficient_to_sim_index( - joint_viscous_friction_coeff=actuator.viscous_friction, - joint_ids=actuator.joint_indices, - ) - - if properties_only: - return - - self.actuators[actuator_name] = actuator - - if isinstance(actuator, ImplicitActuator): - self._has_implicit_actuators = True - self.write_joint_stiffness_to_sim_index(stiffness=actuator.stiffness, joint_ids=actuator.joint_indices) - self.write_joint_damping_to_sim_index(damping=actuator.damping, joint_ids=actuator.joint_indices) - else: - self.write_joint_stiffness_to_sim_index(stiffness=0.0, joint_ids=actuator.joint_indices) - self.write_joint_damping_to_sim_index(damping=0.0, joint_ids=actuator.joint_indices) - - # Store the actuator-configured values in Lab-internal buffers. - # These are separate from the sim-bound model arrays so that - # write_joint_stiffness_to_sim_index(0.0) for explicit actuators - # is not overwritten (the solver must see ke=0 for explicit joints). - j_ids = actuator.joint_indices - if isinstance(j_ids, slice): - j_ids = self._ALL_JOINT_INDICES - has_joint_ordering = self.data.has_joint_ordering - if has_joint_ordering: - joint_armature_user = self.data._joint_armature_user - joint_friction_coeff_user = self.data._joint_friction_coeff_user - else: - joint_armature_user = self.data._sim_bind_joint_armature - joint_friction_coeff_user = self.data._sim_bind_joint_friction_coeff - wp.launch( - shared_kernels.write_2d_data_to_buffer_with_indices_kernel(self._ALL_INDICES, j_ids), - dim=(self.num_instances, j_ids.shape[0]), - inputs=[actuator.stiffness, self._ALL_INDICES, j_ids], - outputs=[self.data._actuator_stiffness], - device=self.device, - ) - wp.launch( - shared_kernels.write_2d_data_to_buffer_with_indices_kernel(self._ALL_INDICES, j_ids), - dim=(self.num_instances, j_ids.shape[0]), - inputs=[actuator.damping, self._ALL_INDICES, j_ids], - outputs=[self.data._actuator_damping], - device=self.device, - ) - ordering_kernels.write_float_user_to_backend_with_indices( - actuator.armature, - self._ALL_INDICES, - j_ids, - self._joint_user_to_backend_map(), - has_joint_ordering, - False, - joint_armature_user, - self.data._sim_bind_joint_armature, - device=self.device, - ) - ordering_kernels.write_float_user_to_backend_with_indices( - actuator.friction, - self._ALL_INDICES, - j_ids, - self._joint_user_to_backend_map(), - has_joint_ordering, - False, - joint_friction_coeff_user, - self.data._sim_bind_joint_friction_coeff, - device=self.device, + """Process actuator configs through :class:`ActuatorCollection`.""" + self._actuator_control = NewtonActuatorControl(self) + self.actuators = ActuatorCollection( + self.cfg.actuators, + self._actuator_control, + debug_value_resolution=self.cfg.actuator_value_resolution_debug_print, ) + self._has_implicit_actuators = self.actuators.has_implicit_actuators + self._has_newton_actuators = self._actuator_control.native_actuator_path_active + self._data.bind_actuator_collection(self.actuators) def _process_tendons(self): """Process fixed and spatial tendons.""" @@ -4143,74 +3467,6 @@ def _process_tendons(self): if tendon_types.sum() > 0: raise NotImplementedError("Spatial tendons are not supported yet.") - def _apply_actuator_model(self): - """Processes joint commands for the articulation by forwarding them to the actuators. - - The actions are first processed using actuator models. Depending on the robot configuration, - the actuator models compute the joint level simulation commands and sets them into the PhysX buffers. - """ - # process actions per group - for actuator in self.actuators.values(): - # prepare input for actuator model based on cached data - actuator_joint_indices = actuator.joint_indices - torch_joint_indices = actuator_joint_indices - # TODO : A tensor dict would be nice to do the indexing of all tensors together - control_action = ArticulationActions( - joint_positions=self._data.joint_pos_target.torch[:, torch_joint_indices], - joint_velocities=self._data.joint_vel_target.torch[:, torch_joint_indices], - joint_efforts=self._data.joint_effort_target.torch[:, torch_joint_indices], - joint_indices=torch_joint_indices, - ) - # compute joint command from the actuator model - control_action = actuator.compute( - control_action, - joint_pos=self._data.joint_pos.torch[:, torch_joint_indices], - joint_vel=self._data.joint_vel.torch[:, torch_joint_indices], - ) - # update targets (these are set into the simulation) - joint_indices = actuator_joint_indices - if isinstance(joint_indices, slice) or joint_indices is None: - joint_indices = self._ALL_JOINT_INDICES - if hasattr(actuator, "gear_ratio"): - gear_ratio = actuator.gear_ratio - else: - gear_ratio = None - wp.launch( - articulation_kernels.update_targets, - dim=(self.num_instances, joint_indices.shape[0]), - inputs=[ - control_action.joint_positions, - control_action.joint_velocities, - control_action.joint_efforts, - joint_indices, - ], - outputs=[ - self._joint_pos_target_sim, - self._joint_vel_target_sim, - self._joint_effort_target_sim, - ], - device=self.device, - ) - # update state of the actuator model - wp.launch( - articulation_kernels.update_actuator_state_model, - dim=(self.num_instances, joint_indices.shape[0]), - inputs=[ - actuator.computed_effort, - actuator.applied_effort, - gear_ratio, - actuator.velocity_limit, - joint_indices, - ], - outputs=[ - self._data.computed_torque, - self._data.applied_torque, - self._data.gear_ratio, - self._data.soft_joint_vel_limits, - ], - device=self.device, - ) - """ Internal helpers -- Debugging. """ diff --git a/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation_data.py b/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation_data.py index b2a6a576edd3..0f027962f302 100644 --- a/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation_data.py +++ b/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation_data.py @@ -370,73 +370,6 @@ def default_joint_vel(self, value: wp.array) -> None: raise ValueError("The articulation data is already primed.") self._default_joint_vel.assign(value) - """ - Joint commands -- Set into simulation. - """ - - @property - def joint_pos_target(self) -> ProxyArray: - """Joint position targets commanded by the user. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - - For an implicit actuator model, the targets are directly set into the simulation. - For an explicit actuator model, the targets are used to compute the joint torques (see :attr:`applied_torque`), - which are then set into the simulation. - """ - return self._joint_pos_target_ta - - @property - def joint_vel_target(self) -> ProxyArray: - """Joint velocity targets commanded by the user. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - - For an implicit actuator model, the targets are directly set into the simulation. - For an explicit actuator model, the targets are used to compute the joint torques (see :attr:`applied_torque`), - which are then set into the simulation. - """ - return self._joint_vel_target_ta - - @property - def joint_effort_target(self) -> ProxyArray: - """Joint effort targets commanded by the user. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - - For an implicit actuator model, the targets are directly set into the simulation. - For an explicit actuator model, the targets are used to compute the joint torques (see :attr:`applied_torque`), - which are then set into the simulation. - """ - return self._joint_effort_target_ta - - """ - Joint commands -- Explicit actuators. - """ - - @property - def computed_torque(self) -> ProxyArray: - """Joint torques computed from the actuator model (before clipping). - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - - This quantity is the raw torque output from the actuator mode, before any clipping is applied. - It is exposed for users who want to inspect the computations inside the actuator model. - For instance, to penalize the learning agent for a difference between the computed and applied torques. - """ - return self._computed_torque_ta - - @property - def applied_torque(self) -> ProxyArray: - """Joint torques applied from the actuator model (after clipping). - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - - These torques are set into the simulation, after clipping the :attr:`computed_torque` based on the - actuator model. - """ - return self._applied_torque_ta - """ Joint properties """ @@ -583,25 +516,6 @@ def soft_joint_pos_limits(self) -> ProxyArray: """ return self._soft_joint_pos_limits_ta - @property - def soft_joint_vel_limits(self) -> ProxyArray: - """Soft joint velocity limits for all joints. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - - These are obtained from the actuator model. It may differ from :attr:`joint_vel_limits` if the actuator model - has a variable velocity limit model. For instance, in a variable gear ratio actuator model. - """ - return self._soft_joint_vel_limits_ta - - @property - def gear_ratio(self) -> ProxyArray: - """Gear ratio for relating motor torques to applied Joint torques. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - """ - return self._gear_ratio_ta - """ Fixed tendon properties. """ @@ -1759,6 +1673,8 @@ def _create_simulation_bindings(self) -> None: self._previous_joint_vel.assign(self._sim_bind_joint_vel) self._previous_body_com_vel.assign(self._sim_bind_body_com_vel_w) reset_timestamps([self._joint_acc, self._body_com_acc_w]) + if self._actuator_collection is not None: + self._actuator_collection._rebind_state_inputs() def _create_buffers(self) -> None: """Create buffers for the root data.""" @@ -1793,12 +1709,6 @@ def _create_buffers(self) -> None: self._default_joint_vel = wp.zeros( (self._num_instances, self._num_joints), dtype=wp.float32, device=self.device ) - # -- joint commands (sent to the actuator from the user) - self._joint_pos_target = wp.zeros((self._num_instances, self._num_joints), dtype=wp.float32, device=self.device) - self._joint_vel_target = wp.zeros((self._num_instances, self._num_joints), dtype=wp.float32, device=self.device) - self._joint_effort_target = wp.zeros( - (self._num_instances, self._num_joints), dtype=wp.float32, device=self.device - ) # -- computed joint efforts from the actuator models self._computed_torque = wp.zeros((self._num_instances, self._num_joints), dtype=wp.float32, device=self.device) self._applied_torque = wp.zeros((self._num_instances, self._num_joints), dtype=wp.float32, device=self.device) @@ -1817,7 +1727,6 @@ def _create_buffers(self) -> None: self._soft_joint_vel_limits = wp.zeros( (self._num_instances, self._num_joints), dtype=wp.float32, device=self.device ) - self._gear_ratio = wp.ones((self._num_instances, self._num_joints), dtype=wp.float32, device=self.device) # -- update the soft joint position limits self._soft_joint_pos_limits = wp.zeros( (self._num_instances, self._num_joints), dtype=wp.vec2f, device=self.device @@ -2418,9 +2327,9 @@ def _pin_proxy_arrays(self) -> None: self._default_root_vel_ta = ProxyArray(self._default_root_vel) self._default_joint_pos_ta = ProxyArray(self._default_joint_pos) self._default_joint_vel_ta = ProxyArray(self._default_joint_vel) - self._joint_pos_target_ta = ProxyArray(self._joint_pos_target) - self._joint_vel_target_ta = ProxyArray(self._joint_vel_target) - self._joint_effort_target_ta = ProxyArray(self._joint_effort_target) + self._joint_pos_target_ta = None + self._joint_vel_target_ta = None + self._joint_effort_target_ta = None self._computed_torque_ta = ProxyArray(self._computed_torque) self._applied_torque_ta = ProxyArray(self._applied_torque) self._joint_stiffness_ta = ProxyArray(joint_stiffness) @@ -2434,7 +2343,6 @@ def _pin_proxy_arrays(self) -> None: self._joint_effort_limits_ta = ProxyArray(joint_effort_limits) self._soft_joint_pos_limits_ta = ProxyArray(self._soft_joint_pos_limits) self._soft_joint_vel_limits_ta = ProxyArray(self._soft_joint_vel_limits) - self._gear_ratio_ta = ProxyArray(self._gear_ratio) body_mass = self._body_mass_user if self.has_body_ordering else self._sim_bind_body_mass body_inertia = self._body_inertia_user if self.has_body_ordering else self._sim_bind_body_inertia body_com_pos_b = self._body_com_pos_b_user if self.has_body_ordering else self._sim_bind_body_com_pos_b diff --git a/source/isaaclab_newton/isaaclab_newton/assets/articulation/kernels.py b/source/isaaclab_newton/isaaclab_newton/assets/articulation/kernels.py index 19f61975913a..eb90cb795529 100644 --- a/source/isaaclab_newton/isaaclab_newton/assets/articulation/kernels.py +++ b/source/isaaclab_newton/isaaclab_newton/assets/articulation/kernels.py @@ -630,52 +630,6 @@ def update_targets( target_joint_efforts[i, joint_indices[j]] = source_joint_efforts[i, j] -@wp.kernel -def update_actuator_state_model( - source_computed_effort: wp.array2d(dtype=wp.float32), - source_applied_effort: wp.array2d(dtype=wp.float32), - source_gear_ratio: wp.array2d(dtype=wp.float32), - source_vel_limits: wp.array2d(dtype=wp.float32), - joint_indices: wp.array(dtype=wp.int32), - target_computed_effort: wp.array2d(dtype=wp.float32), - target_applied_effort: wp.array2d(dtype=wp.float32), - target_gear_ratio: wp.array2d(dtype=wp.float32), - target_soft_joint_vel_limits: wp.array2d(dtype=wp.float32), -): - """Update actuator state model parameters from source arrays using joint indices. - - This kernel copies actuator state model parameters (computed effort, applied effort, - gear ratio, and velocity limits) from source arrays to target arrays, remapping - joint indices using the provided joint_indices array. - - Args: - source_computed_effort: Input array of source computed effort values. Shape is - (num_envs, num_selected_joints). - source_applied_effort: Input array of source applied effort values. Shape is - (num_envs, num_selected_joints). - source_gear_ratio: Input array of source gear ratio values. Shape is - (num_envs, num_selected_joints). Can be None if not provided. - source_vel_limits: Input array of source velocity limit values. Shape is - (num_envs, num_selected_joints). - joint_indices: Input array of joint indices for remapping. Shape is - (num_selected_joints,). Specifies which joints in the target arrays to update. - target_computed_effort: Output array where computed effort values are written. - Shape is (num_envs, num_joints). - target_applied_effort: Output array where applied effort values are written. - Shape is (num_envs, num_joints). - target_gear_ratio: Output array where gear ratio values are written. Shape is - (num_envs, num_joints). - target_soft_joint_vel_limits: Output array where soft joint velocity limits are - written. Shape is (num_envs, num_joints). - """ - i, j = wp.tid() - target_computed_effort[i, joint_indices[j]] = source_computed_effort[i, j] - target_applied_effort[i, joint_indices[j]] = source_applied_effort[i, j] - target_soft_joint_vel_limits[i, joint_indices[j]] = source_vel_limits[i, j] - if source_gear_ratio: - target_gear_ratio[i, joint_indices[j]] = source_gear_ratio[i, j] - - @wp.kernel def extract_friction_properties( friction_props: wp.array3d(dtype=wp.float32), diff --git a/source/isaaclab_newton/isaaclab_newton/benchmark/assets/runtime.py b/source/isaaclab_newton/isaaclab_newton/benchmark/assets/runtime.py index a3d49f7d3240..eaff57094617 100644 --- a/source/isaaclab_newton/isaaclab_newton/benchmark/assets/runtime.py +++ b/source/isaaclab_newton/isaaclab_newton/benchmark/assets/runtime.py @@ -83,6 +83,7 @@ def create_test_articulation( object.__setattr__(articulation, "_root_view", mock_view) object.__setattr__(articulation, "_device", device) object.__setattr__(articulation, "_check_shapes", not args.no_shape_checks) + object.__setattr__(articulation, "_sim_cfg", SimpleNamespace(use_newton_actuators=False)) from isaaclab_newton.assets.articulation import articulation_data as data_module @@ -113,17 +114,13 @@ def create_test_articulation( object.__setattr__(articulation, "_ALL_SPATIAL_TENDON_INDICES", wp.array([], dtype=wp.int32, device=device)) object.__setattr__(articulation, "_ALL_SPATIAL_TENDON_MASK", wp.zeros((0,), dtype=wp.bool, device=device)) - object.__setattr__( - articulation, "_joint_pos_target_sim", wp.zeros((num_instances, num_joints), dtype=wp.float32, device=device) - ) - object.__setattr__( - articulation, "_joint_vel_target_sim", wp.zeros((num_instances, num_joints), dtype=wp.float32, device=device) - ) - object.__setattr__( - articulation, - "_joint_effort_target_sim", - wp.zeros((num_instances, num_joints), dtype=wp.float32, device=device), - ) + from isaaclab.actuators import ActuatorCollection + + from isaaclab_newton.assets.articulation.actuator_control import NewtonActuatorControl + + control = NewtonActuatorControl(articulation) + object.__setattr__(articulation, "actuators", ActuatorCollection({}, control)) + data.bind_actuator_collection(articulation.actuators) return articulation, mock_view diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index 17eface9b9ea..e1a77bbcd43c 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -108,7 +108,8 @@ def _paused_gc(): from isaaclab_newton.physics.xpbd_manager_cfg import XPBDSolverCfg if TYPE_CHECKING: - from isaaclab_newton.actuators import NewtonActuatorAdapter + from isaaclab.actuators.newton import NewtonActuatorAdapter + from isaaclab_newton.physics.newton_collision_cfg import NewtonCollisionPipelineCfg logger = logging.getLogger(__name__) @@ -2132,17 +2133,10 @@ def initialize_solver(cls) -> None: cls._eval_fk(None, None) cls._mark_transforms_dirty() - # Skip the initial graph capture when the Newton actuator fast path is - # active. Capturing here would use ``cls._decimation`` (still its default - # of 1, because the env's ``set_decimation`` hasn't run yet); a second - # capture from ``set_decimation`` then triggers an illegal-memory-access - # CUDA fault inside the captured ``_simulate_full`` graph (back-to-back - # captures of the contact + actuator pipeline don't survive re-capture - # — root cause is in Newton's collision/actuator buffer handling, not - # Lab code). For non-Newton-actuator paths this branch is unaffected: - # ``set_decimation`` is a no-op for them (``_is_all_graphable`` is False), - # so we still need the start-time capture below. - if not cls._use_newton_actuators_active: + # Fully graphable Newton actuators defer capture until ``set_decimation`` + # provides the environment's final decimation value. Other paths capture + # the solver here; non-graphable actuators otherwise leave it eager. + if not cls._is_all_graphable(): cls._capture_or_defer_graph() @classmethod @@ -3139,7 +3133,7 @@ def activate_newton_actuator_path(cls) -> None: return if cls._model is None or not cls._model.actuators: return - from isaaclab_newton.actuators import NewtonActuatorAdapter # noqa: PLC0415 + from isaaclab.actuators.newton import NewtonActuatorAdapter # noqa: PLC0415 dofs_per_env = cls._model.joint_dof_count // cls._num_envs NewtonManager._adapter = NewtonActuatorAdapter( diff --git a/source/isaaclab_newton/test/assets/test_articulation.py b/source/isaaclab_newton/test/assets/test_articulation.py index bdb7ef2c677a..42ef2d56fbc8 100644 --- a/source/isaaclab_newton/test/assets/test_articulation.py +++ b/source/isaaclab_newton/test/assets/test_articulation.py @@ -37,6 +37,7 @@ import torch import warp as wp from isaaclab_newton.assets import Articulation +from isaaclab_newton.assets.articulation.actuator_control import NewtonActuatorControl from isaaclab_newton.assets.articulation.articulation_data import ArticulationData from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg from isaaclab_newton.physics import NewtonManager as SimulationManager @@ -50,7 +51,11 @@ import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils import isaaclab.utils.string as string_utils -from isaaclab.actuators import ActuatorBase, IdealPDActuatorCfg, ImplicitActuatorCfg +from isaaclab.actuators import ( + IdealPDActuatorCfg, + ImplicitActuator, + ImplicitActuatorCfg, +) from isaaclab.assets import ArticulationCfg from isaaclab.assets.articulation.ordering_resolvers import get_articulation_name_ordering from isaaclab.controllers import ( @@ -188,14 +193,18 @@ } +class CustomDrive(ImplicitActuator): + """Implicit actuator with a class name that does not encode its execution type.""" + + def generate_articulation_cfg( articulation_type: str, stiffness: float | None = 10.0, damping: float | None = 2.0, - velocity_limit: float | None = None, - effort_limit: float | None = None, - velocity_limit_sim: float | None = None, - effort_limit_sim: float | None = None, + 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. @@ -207,13 +216,13 @@ def generate_articulation_cfg( Defaults to 10.0. damping: Damping value for the articulation's actuators. Only currently used for "humanoid". Defaults to 2.0. - velocity_limit: Velocity limit for the actuators. Only currently used for "single_joint_implicit" + actuator_velocity_limit: Velocity limit for the actuators. Only currently used for "single_joint_implicit" and "single_joint_explicit". - effort_limit: Effort limit for the actuators. Only currently used for "single_joint_implicit" - and "single_joint_explicit". - velocity_limit_sim: Velocity limit for the actuators (set into the simulation). + 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". - effort_limit_sim: Effort limit for the actuators (set into the simulation). + joint_effort_limit: Effort limit for the actuators (set into the simulation). Only currently used for "single_joint_implicit" and "single_joint_explicit". Returns: @@ -245,10 +254,9 @@ def generate_articulation_cfg( actuators={ "joint": ImplicitActuatorCfg( joint_names_expr=[".*"], - effort_limit_sim=effort_limit_sim, - velocity_limit_sim=velocity_limit_sim, - effort_limit=effort_limit, - velocity_limit=velocity_limit, + joint_effort_limit=joint_effort_limit, + joint_velocity_limit=joint_velocity_limit, + actuator_velocity_limit=actuator_velocity_limit, stiffness=2000.0, damping=100.0, ), @@ -269,10 +277,10 @@ def generate_articulation_cfg( actuators={ "joint": IdealPDActuatorCfg( joint_names_expr=[".*"], - effort_limit_sim=effort_limit_sim, - velocity_limit_sim=velocity_limit_sim, - effort_limit=effort_limit, - velocity_limit=velocity_limit, + 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, ), @@ -645,6 +653,27 @@ def test_viscous_writer_updates_finalized_newton_model(monkeypatch): torch.testing.assert_close(torch.from_numpy(model.joint_damping.numpy()), torch.tensor([0.25])) +def test_prepare_native_actuators_does_not_zero_solver_gains(monkeypatch): + """Leave solver gains untouched until collection construction resolves actuator defaults.""" + gain_writes = [] + articulation = SimpleNamespace( + _sim_cfg=SimpleNamespace(use_newton_actuators=True), + device="cpu", + find_joints=lambda _: ([0], ["joint"]), + write_joint_stiffness_to_sim_index=lambda **_: gain_writes.append("stiffness"), + write_joint_damping_to_sim_index=lambda **_: gain_writes.append("damping"), + ) + monkeypatch.setattr(SimulationManager, "activate_newton_actuator_path", lambda: None) + + native_groups = NewtonActuatorControl(articulation).prepare_native_actuators( + collection=None, + actuator_cfgs={"explicit": IdealPDActuatorCfg(joint_names_expr=["joint"], stiffness=None, damping=None)}, + ) + + assert native_groups == {"explicit"} + assert gain_writes == [] + + @pytest.mark.parametrize( ("actuator_cfg", "expected_mode", "expected_actuator_indices"), [ @@ -663,6 +692,13 @@ def test_viscous_writer_updates_finalized_newton_model(monkeypatch): JointTargetMode.POSITION_VELOCITY, [0, -2, 1, -3], ), + ( + ImplicitActuatorCfg( + class_type=f"{__name__}:CustomDrive", joint_names_expr=[".*"], stiffness=10.0, damping=2.0 + ), + JointTargetMode.POSITION_VELOCITY, + [0, -2, 1, -3], + ), ( ImplicitActuatorCfg(joint_names_expr=[".*"], stiffness=0.0, damping=0.0), JointTargetMode.EFFORT, @@ -957,54 +993,52 @@ class _ShapeCountSurface: assert articulation.num_shapes_per_body == [3, 0, 2] -@pytest.mark.parametrize("num_articulations", [2]) @pytest.mark.parametrize("device", ["cuda:0", "cpu"]) -@pytest.mark.parametrize("gravity_enabled", [False]) -@pytest.mark.parametrize("articulation_type", ["anymal"]) -@pytest.mark.parametrize("use_newton_actuators", [True]) -def test_newton_actuator_gain_writes_map_public_joint_subset_to_backend( - sim, num_articulations, device, gravity_enabled, articulation_type, use_newton_actuators +@pytest.mark.parametrize("articulation_type", ["anymal"]) # consumed by the sim fixture +@pytest.mark.parametrize("use_newton_actuators", [True]) # consumed by the sim fixture +def test_newton_native_actuator_gain_write_maps_public_joint_subset_to_backend( + sim, articulation_type, use_newton_actuators, device ): - """Map partial public-order gain writes to Newton controller backend columns.""" - articulation_cfg = generate_articulation_cfg(articulation_type).replace( + """Map selected public joint IDs to Newton-controller columns.""" + articulation_cfg = generate_articulation_cfg("anymal").replace( actuators={ "legs": IdealPDActuatorCfg( joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], stiffness=40.0, damping=5.0, - effort_limit=80.0, + actuator_effort_limit=80.0, ) }, joint_ordering=tuple(reversed(ANYMAL_C_PHYSX_JOINT_NAMES)), ) - articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) + articulation, _ = generate_articulation(articulation_cfg, 2, device=sim.device) sim.reset() - - assert use_newton_actuators assert articulation.joint_ordering is not None assert articulation.newton_actuator_adapter is not None - def gather_controller_parameter(name: str) -> torch.Tensor: - parameter = torch.zeros( + def gather_stiffness() -> torch.Tensor: + stiffness = torch.zeros( (articulation.num_instances, articulation.num_joints), device=articulation.device, ) for actuator in articulation.newton_actuator_adapter.actuators: - if hasattr(actuator.controller, name): - parameter += wp.to_torch( - articulation.root_view.get_actuator_parameter(actuator, actuator.controller, name) + if hasattr(actuator.controller, "kp"): + stiffness += wp.to_torch( + articulation.root_view.get_actuator_parameter(actuator, actuator.controller, "kp") ) - return parameter + return stiffness - stiffness_before = gather_controller_parameter("kp").clone() - damping_before = gather_controller_parameter("kd").clone() + stiffness_before = gather_stiffness() env_ids = torch.tensor([1], device=articulation.device, dtype=torch.long) joint_ids = torch.tensor([1, 6, 10], device=articulation.device, dtype=torch.long) stiffness = torch.tensor([[101.0, 106.0, 110.0]], device=articulation.device) - damping = torch.tensor([[11.0, 16.0, 20.0]], device=articulation.device) - articulation.write_actuator_stiffness_to_sim(stiffness=stiffness, env_ids=env_ids, joint_ids=joint_ids) - articulation.write_actuator_damping_to_sim(damping=damping, env_ids=env_ids, joint_ids=joint_ids) + with pytest.warns(DeprecationWarning, match="write_actuator_stiffness_to_sim"): + articulation.write_actuator_stiffness_to_sim( + stiffness=stiffness, + env_ids=env_ids, + joint_ids=joint_ids, + ) backend_joint_ids = torch.tensor( articulation.joint_ordering.user_to_backend_indices, @@ -1012,11 +1046,8 @@ def gather_controller_parameter(name: str) -> torch.Tensor: dtype=torch.long, )[joint_ids] expected_stiffness = stiffness_before.clone() - expected_damping = damping_before.clone() expected_stiffness[env_ids.unsqueeze(1), backend_joint_ids.unsqueeze(0)] = stiffness - expected_damping[env_ids.unsqueeze(1), backend_joint_ids.unsqueeze(0)] = damping - torch.testing.assert_close(gather_controller_parameter("kp"), expected_stiffness) - torch.testing.assert_close(gather_controller_parameter("kd"), expected_damping) + torch.testing.assert_close(gather_stiffness(), expected_stiffness) @pytest.mark.parametrize("num_articulations", [1]) @@ -1140,6 +1171,9 @@ def test_newton_ordered_state_caches_invalidate_on_rebind( old_bindings = {name: getattr(data, name) for name in public_to_binding.values()} old_binding_ptrs = {name: int(array.ptr) for name, array in old_bindings.items()} old_public_proxies = {name: getattr(data, name) for name in public_to_binding} + implicit_executor = articulation.actuators._implicit_executor + assert implicit_executor is not None + actuator_state_inputs = [implicit_executor.kernel_inputs] data.joint_pos_limits.torch.clone() assert data._joint_pos_limits_timestamp == data._sim_timestamp # The Tier-1 state shadows are plain wp.arrays (no timestamp): they are @@ -1202,6 +1236,10 @@ def test_newton_ordered_state_caches_invalidate_on_rebind( assert int(rebound.ptr) != old_binding_ptrs[binding_name] assert int(rebound.ptr) == int(new_source_bindings[binding_name].ptr) + for inputs in actuator_state_inputs: + assert inputs[3].ptr == data.joint_pos.warp.ptr + assert inputs[4].ptr == data.joint_vel.warp.ptr + assert data._joint_pos_limits_timestamp == -1.0 assert data._joint_acc.timestamp == -1.0 assert data._body_com_acc_w.timestamp == -1.0 @@ -1272,10 +1310,9 @@ def test_newton_rebind_preserves_lab_owned_actuator_gains( ): """Keep Lab-owned actuator gains across a rebind that re-seeds the solver's sim gains. - Part 2 (D3) regression: ``_actuator_stiffness`` / ``_actuator_damping`` are Lab-owned - records (the actuator kp/kd); the solver's sim gains are deliberately zeroed for - explicit DOFs. A full sim reset recreates the solver arrays, and rebind must NOT - resync the Lab-owned records from the freshly rebuilt (here: sentinel) solver gains. + Part 2 (D3) regression: named actuator groups own their actuator kp/kd; the solver's + sim gains are deliberately zeroed for explicit DOFs. A full sim reset recreates the solver arrays. + Rebind must NOT resync the actuator-owned values from freshly rebuilt (here: sentinel) solver gains. ``none`` is the identity-ordering control that must pass with or without the fix. """ articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type).replace( @@ -1284,7 +1321,7 @@ def test_newton_rebind_preserves_lab_owned_actuator_gains( joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], stiffness=40.0, damping=5.0, - effort_limit=80.0, + actuator_effort_limit=80.0, ) }, ) @@ -1298,10 +1335,10 @@ def test_newton_rebind_preserves_lab_owned_actuator_gains( data = articulation.data assert (data.joint_ordering is not None) is has_ordering - # Prime: explicit (IdealPD) actuators keep their PD in the Lab-owned records, + # Prime: explicit (IdealPD) actuators keep their PD in actuator-owned records, # while the solver's sim gains are zeroed so it applies no PD on these DOFs. - np.testing.assert_allclose(data._actuator_stiffness.numpy(), 40.0) - np.testing.assert_allclose(data._actuator_damping.numpy(), 5.0) + np.testing.assert_allclose(articulation.actuators["legs"].stiffness.cpu().numpy(), 40.0) + np.testing.assert_allclose(articulation.actuators["legs"].damping.cpu().numpy(), 5.0) np.testing.assert_allclose(data._sim_bind_joint_stiffness_sim.numpy(), 0.0) np.testing.assert_allclose(data._sim_bind_joint_damping_sim.numpy(), 0.0) @@ -1325,9 +1362,9 @@ def test_newton_rebind_preserves_lab_owned_actuator_gains( SimulationManager._model = new_model data._create_simulation_bindings() - # The Lab-owned actuator gains must survive the rebind unchanged... - np.testing.assert_allclose(data._actuator_stiffness.numpy(), 40.0) - np.testing.assert_allclose(data._actuator_damping.numpy(), 5.0) + # The actuator-owned gains must survive the rebind unchanged... + np.testing.assert_allclose(articulation.actuators["legs"].stiffness.cpu().numpy(), 40.0) + np.testing.assert_allclose(articulation.actuators["legs"].damping.cpu().numpy(), 5.0) # ...while the sim-owned mirrors track the solver's freshly seeded (sentinel) gains. if has_ordering: np.testing.assert_allclose(data._joint_stiffness_user.numpy(), sentinel_ke) @@ -1444,17 +1481,7 @@ def _other_callback() -> None: def test_write_data_to_sim_gathers_joint_targets_only_when_ordering_active( sim, num_articulations, device, gravity_enabled, articulation_type, use_newton_actuators, ordering_mode, monkeypatch ): - """Launch the fused target gather only under active ordering; copy straight through otherwise. - - Regression for the identity fast path: an earlier rework launched - :func:`ordering_kernels.reorder_joint_targets_user_to_backend` unconditionally - in :meth:`write_data_to_sim`, so a scene with no ordering configured paid for a - per-step gather that the pre-ordering code never issued. This test records the - kernels launched during ``write_data_to_sim`` and asserts the target gather runs - only when ordering is active. With the unconditional launch reinstated, the - ``ordering_mode == "none"`` cases fail (the gather is recorded). Both the - Newton-actuator and Lab-actuator branches are covered. - """ + """Gather joint targets only when non-identity joint ordering is active.""" articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type).replace( actuators={"legs": ImplicitActuatorCfg(joint_names_expr=[".*"], stiffness=40.0, damping=5.0)}, ) @@ -1490,10 +1517,13 @@ def recording_launch(kernel, *args, **kwargs): if has_ordering: assert target_gather in launched_kernels else: - # Identity ordering copies straight into the sim binds -- no target gather, - # and the sim-bound position target mirrors its user-order source. + # Identity ordering binds the user-order source directly. assert target_gather not in launched_kernels - expected_source = articulation.data._joint_pos_target if on_newton_path else articulation._joint_pos_target_sim + expected_source = ( + articulation.actuators.target_command.position.warp + if on_newton_path + else articulation.actuators.output_command.position.warp + ) np.testing.assert_allclose(articulation.data._sim_bind_joint_position_target.numpy(), expected_source.numpy()) @@ -2733,33 +2763,32 @@ def test_setting_gains_from_cfg_dict(sim, num_articulations, device, articulatio @pytest.mark.parametrize("num_articulations", [1, 2]) @pytest.mark.parametrize("device", test_devices()) -@pytest.mark.parametrize("vel_limit_sim", [1e5, None]) +@pytest.mark.parametrize("joint_velocity_limit", [1e5, None]) @pytest.mark.parametrize("vel_limit", [1e2, None]) -@pytest.mark.parametrize("add_ground_plane", [False]) -@pytest.mark.parametrize("articulation_type", ["single_joint_implicit"]) +@pytest.mark.parametrize("articulation_type", ["single_joint_implicit"]) # consumed by the sim fixture def test_setting_velocity_limit_implicit( - sim, num_articulations, device, vel_limit_sim, vel_limit, add_ground_plane, articulation_type + sim, articulation_type, num_articulations, device, joint_velocity_limit, vel_limit ): """Test setting of velocity limit for implicit actuators. This test verifies that: - 1. The solver clamp ``velocity_limit_sim`` is applied to the simulation; when unset, the + 1. The solver clamp ``joint_velocity_limit`` is applied to the simulation; when unset, the USD-authored value is kept - 2. The joint velocity limit ``velocity_limit`` is never pushed to the solver and keeps its + 2. The actuator velocity limit ``actuator_velocity_limit`` is never pushed to the solver and keeps its configured value; when unset, it falls back to the solver clamp Args: sim: The simulation fixture num_articulations: Number of articulations to test device: The device to run the simulation on - vel_limit_sim: The velocity limit to set in simulation + joint_velocity_limit: The velocity limit to set in simulation vel_limit: The velocity limit to set in actuator """ # create simulation articulation_cfg = generate_articulation_cfg( - articulation_type=articulation_type, - velocity_limit_sim=vel_limit_sim, - velocity_limit=vel_limit, + articulation_type="single_joint_implicit", + joint_velocity_limit=joint_velocity_limit, + actuator_velocity_limit=vel_limit, ) articulation, _ = generate_articulation( articulation_cfg=articulation_cfg, @@ -2774,15 +2803,12 @@ def test_setting_velocity_limit_implicit( articulation.root_view.get_attribute("joint_velocity_limit", SimulationManager.get_model()) ).to(device)[:, 0, :] # check data buffer - torch.testing.assert_close(articulation.data.joint_velocity_limits.torch, newton_vel_limit) - # check actuator has simulation velocity limit - torch.testing.assert_close(articulation.actuators["joint"].velocity_limit_sim, newton_vel_limit) - - # the solver clamp comes from velocity_limit_sim when set, otherwise the USD-authored value - if vel_limit_sim is None: + torch.testing.assert_close(articulation.data.joint_vel_limits.torch, newton_vel_limit) + # the solver clamp comes from joint_velocity_limit when set, otherwise the USD-authored value + if joint_velocity_limit is None: sim_limit = articulation_cfg.spawn.joint_drive_props.max_joint_velocity else: - sim_limit = vel_limit_sim + sim_limit = joint_velocity_limit expected_velocity_limit = torch.full_like(newton_vel_limit, sim_limit) torch.testing.assert_close(newton_vel_limit, expected_velocity_limit) @@ -2790,20 +2816,22 @@ def test_setting_velocity_limit_implicit( # when unset it falls back to the solver clamp joint_limit = vel_limit if vel_limit is not None else sim_limit expected_joint_limit = torch.full_like(newton_vel_limit, joint_limit) - torch.testing.assert_close(articulation.actuators["joint"].velocity_limit, expected_joint_limit) + torch.testing.assert_close(articulation.actuators["joint"].actuator_velocity_limit, expected_joint_limit) @pytest.mark.parametrize("num_articulations", [1, 2]) @pytest.mark.parametrize("device", test_devices()) -@pytest.mark.parametrize("vel_limit_sim", [1e5, None]) +@pytest.mark.parametrize("joint_velocity_limit", [1e5, None]) @pytest.mark.parametrize("vel_limit", [1e2, None]) -@pytest.mark.parametrize("articulation_type", ["single_joint_explicit"]) -def test_setting_velocity_limit_explicit(sim, num_articulations, device, vel_limit_sim, vel_limit, articulation_type): +@pytest.mark.parametrize("articulation_type", ["single_joint_explicit"]) # consumed by the sim fixture +def test_setting_velocity_limit_explicit( + sim, articulation_type, num_articulations, device, joint_velocity_limit, vel_limit +): """Test setting of velocity limit for explicit actuators.""" articulation_cfg = generate_articulation_cfg( - articulation_type=articulation_type, - velocity_limit_sim=vel_limit_sim, - velocity_limit=vel_limit, + articulation_type="single_joint_explicit", + joint_velocity_limit=joint_velocity_limit, + actuator_velocity_limit=vel_limit, ) articulation, _ = generate_articulation( articulation_cfg=articulation_cfg, @@ -2817,13 +2845,10 @@ def test_setting_velocity_limit_explicit(sim, num_articulations, device, vel_lim newton_vel_limit = wp.to_torch( articulation.root_view.get_attribute("joint_velocity_limit", SimulationManager.get_model()) ).to(device)[:, 0, :] - actuator_vel_limit = articulation.actuators["joint"].velocity_limit - actuator_vel_limit_sim = articulation.actuators["joint"].velocity_limit_sim + actuator_vel_limit = articulation.actuators["joint"].actuator_velocity_limit - # check data buffer for joint_velocity_limits_sim - torch.testing.assert_close(articulation.data.joint_velocity_limits.torch, newton_vel_limit) - # check actuator velocity_limit_sim is set to physx - torch.testing.assert_close(actuator_vel_limit_sim, newton_vel_limit) + # check data buffer for joint_vel_limits + torch.testing.assert_close(articulation.data.joint_vel_limits.torch, newton_vel_limit) if vel_limit is not None: expected_actuator_vel_limit = torch.full( @@ -2833,15 +2858,15 @@ def test_setting_velocity_limit_explicit(sim, num_articulations, device, vel_lim ) # check actuator is set torch.testing.assert_close(actuator_vel_limit, expected_actuator_vel_limit) - # check physx is not velocity_limit + # check physx is not actuator_velocity_limit assert not torch.allclose(actuator_vel_limit, newton_vel_limit) else: - # check actuator velocity_limit is the same as the PhysX default + # check actuator_velocity_limit is the same as the PhysX default torch.testing.assert_close(actuator_vel_limit, newton_vel_limit) # simulation velocity limit is set to USD value unless user overrides - if vel_limit_sim is not None: - limit = vel_limit_sim + if joint_velocity_limit is not None: + limit = joint_velocity_limit else: limit = articulation_cfg.spawn.joint_drive_props.max_joint_velocity # check physx is set to expected value @@ -2851,12 +2876,9 @@ def test_setting_velocity_limit_explicit(sim, num_articulations, device, vel_lim @pytest.mark.parametrize("num_articulations", [1, 2]) @pytest.mark.parametrize("device", test_devices()) -@pytest.mark.parametrize("effort_limit_sim", [1e5, None]) -@pytest.mark.parametrize("effort_limit", [1e2, 80.0, None]) -@pytest.mark.parametrize("articulation_type", ["single_joint_implicit"]) -def test_setting_effort_limit_implicit( - sim, num_articulations, device, effort_limit_sim, effort_limit, articulation_type -): +@pytest.mark.parametrize("joint_effort_limit", [1e5, None]) +@pytest.mark.parametrize("articulation_type", ["single_joint_implicit"]) # consumed by the sim fixture +def test_setting_effort_limit_implicit(sim, articulation_type, num_articulations, device, joint_effort_limit): """Test setting of effort limit for implicit actuators. This test verifies the effort limit resolution logic for actuator models implemented in :class:`ActuatorBase`: @@ -2865,9 +2887,8 @@ def test_setting_effort_limit_implicit( - Case 3: If actuator config value is None: USD value is used as default """ articulation_cfg = generate_articulation_cfg( - articulation_type=articulation_type, - effort_limit_sim=effort_limit_sim, - effort_limit=effort_limit, + articulation_type="single_joint_implicit", + joint_effort_limit=joint_effort_limit, ) articulation, _ = generate_articulation( articulation_cfg=articulation_cfg, @@ -2882,26 +2903,29 @@ def test_setting_effort_limit_implicit( articulation.root_view.get_attribute("joint_effort_limit", SimulationManager.get_model()) ).to(device)[:, 0, :] - # The solver clamp reaches the physics engine; the rated limit remains on the actuator. - torch.testing.assert_close(articulation.actuators["joint"].effort_limit_sim, newton_effort_limit) + torch.testing.assert_close(articulation.data.joint_effort_limits.torch, newton_effort_limit) + torch.testing.assert_close(articulation.actuators["joint"].joint_effort_limit, newton_effort_limit) + # without a separately configured rated limit, the actuator limit tracks the solver clamp + torch.testing.assert_close(articulation.actuators["joint"].actuator_effort_limit, newton_effort_limit) - solver_limit = effort_limit_sim if effort_limit_sim is not None else effort_limit - if solver_limit is None: - solver_limit = articulation_cfg.spawn.joint_drive_props.max_force - rated_limit = effort_limit if effort_limit is not None else solver_limit - torch.testing.assert_close(newton_effort_limit, torch.full_like(newton_effort_limit, solver_limit)) - torch.testing.assert_close( - articulation.actuators["joint"].effort_limit, torch.full_like(newton_effort_limit, rated_limit) - ) + # decide the limit based on what is set + if joint_effort_limit is None: + limit = articulation_cfg.spawn.joint_drive_props.max_force + else: + limit = joint_effort_limit + + # check that the max force is what we set + expected_effort_limit = torch.full_like(newton_effort_limit, limit) + torch.testing.assert_close(newton_effort_limit, expected_effort_limit) @pytest.mark.parametrize("num_articulations", [1, 2]) @pytest.mark.parametrize("device", test_devices()) -@pytest.mark.parametrize("effort_limit_sim", [1e5, None]) -@pytest.mark.parametrize("effort_limit", [80.0, 1e2, None]) -@pytest.mark.parametrize("articulation_type", ["single_joint_explicit"]) +@pytest.mark.parametrize("joint_effort_limit", [1e5, None]) +@pytest.mark.parametrize("actuator_effort_limit", [1e2, None]) +@pytest.mark.parametrize("articulation_type", ["single_joint_explicit"]) # consumed by the sim fixture def test_setting_effort_limit_explicit( - sim, num_articulations, device, effort_limit_sim, effort_limit, articulation_type + sim, articulation_type, num_articulations, device, joint_effort_limit, actuator_effort_limit ): """Test setting of effort limit for explicit actuators. @@ -2913,9 +2937,9 @@ def test_setting_effort_limit_explicit( """ articulation_cfg = generate_articulation_cfg( - articulation_type=articulation_type, - effort_limit_sim=effort_limit_sim, - effort_limit=effort_limit, + articulation_type="single_joint_explicit", + joint_effort_limit=joint_effort_limit, + actuator_effort_limit=actuator_effort_limit, ) articulation, _ = generate_articulation( articulation_cfg=articulation_cfg, @@ -2932,32 +2956,26 @@ def test_setting_effort_limit_explicit( newton_effort_limit = wp.to_torch( articulation.root_view.get_attribute("joint_effort_limit", SimulationManager.get_model()) ).to(device)[:, 0, :] - actuator_effort_limit = articulation.actuators["joint"].effort_limit - actuator_effort_limit_sim = articulation.actuators["joint"].effort_limit_sim - - # check actuator effort_limit_sim is set to physx - torch.testing.assert_close(actuator_effort_limit_sim, newton_effort_limit) + actuator_effort_limit_actual = articulation.actuators["joint"].actuator_effort_limit - if effort_limit is not None: - expected_actuator_effort_limit = torch.full_like(actuator_effort_limit, effort_limit) + if actuator_effort_limit is not None: + expected_actuator_effort_limit = torch.full_like(actuator_effort_limit_actual, actuator_effort_limit) # check actuator is set - torch.testing.assert_close(actuator_effort_limit, expected_actuator_effort_limit) + torch.testing.assert_close(actuator_effort_limit_actual, expected_actuator_effort_limit) - # check physx effort limit does not match the one explicit actuator has - assert not (torch.allclose(actuator_effort_limit, newton_effort_limit)) else: - # When effort_limit is None, actuator should use USD default values + # When actuator_effort_limit is None, actuator should use USD default values expected_actuator_effort_limit = torch.full_like(newton_effort_limit, usd_default_effort_limit) - torch.testing.assert_close(actuator_effort_limit, expected_actuator_effort_limit) + torch.testing.assert_close(actuator_effort_limit_actual, expected_actuator_effort_limit) - # when using explicit actuators, the limits are set to high unless user overrides - if effort_limit_sim is not None: - limit = effort_limit_sim + # the solver keeps the authored limit unless the user overrides it explicitly + if joint_effort_limit is not None: + limit = joint_effort_limit else: - limit = ActuatorBase._DEFAULT_MAX_EFFORT_SIM # type: ignore + limit = usd_default_effort_limit # check physx internal value matches the expected sim value expected_effort_limit = torch.full_like(newton_effort_limit, limit) - torch.testing.assert_close(actuator_effort_limit_sim, expected_effort_limit) + torch.testing.assert_close(articulation.data.joint_effort_limits.torch, expected_effort_limit) torch.testing.assert_close(newton_effort_limit, expected_effort_limit) diff --git a/source/isaaclab_newton/test/assets/test_newton_actuators_newton.py b/source/isaaclab_newton/test/assets/test_newton_actuators_newton.py index ad3da92a9081..1e99e95edd88 100644 --- a/source/isaaclab_newton/test/assets/test_newton_actuators_newton.py +++ b/source/isaaclab_newton/test/assets/test_newton_actuators_newton.py @@ -22,27 +22,40 @@ simulation_app = AppLauncher(headless=True).app -import json +import functools import os -import tempfile -import types import unittest import numpy as np -import pytest import torch import warp as wp -from isaaclab_newton.actuators.kernels import sync_torque_telemetry from isaaclab_newton.assets import Articulation from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg from isaaclab_newton.physics import NewtonManager as SimulationManager import isaaclab.sim as sim_utils -from isaaclab.actuators import DCMotorCfg, DelayedPDActuatorCfg, IdealPDActuatorCfg, ImplicitActuatorCfg +from isaaclab.actuators import IdealPDActuatorCfg +from isaaclab.actuators.newton import read_group_parameter +from isaaclab.actuators.newton.kernels import sync_torque_telemetry from isaaclab.sim import SimulationCfg, build_simulation_context +from isaaclab.test.utils.actuator_equivalence import ( + CARTPOLE_EXPLICIT_ACTUATORS, + DC_MOTOR_ACTUATORS, + DELAYED_PD_ACTUATORS, + IDEAL_PD_ACTUATORS, + IMPLICIT_ONLY_ACTUATORS, + MIXED_WITH_IMPLICIT_ACTUATORS, + ActuatorStateResetBase, + EquivalenceAssertionsMixin, + MockEnv, + build_dr_term, + make_dummy_lstm_checkpoint, + make_dummy_mlp_checkpoint, +) from isaaclab.test.utils.articulation_ordering import assert_articulation_ordering_trace_matches from isaaclab_assets import ANYMAL_C_CFG +from isaaclab_assets.robots.spot import joint_parameter_lookup as SPOT_KNEE_LOOKUP # --------------------------------------------------------------------------- # Constants @@ -67,47 +80,6 @@ use_cuda_graph=False, ) -# --------------------------------------------------------------------------- -# Actuator configurations under test -# --------------------------------------------------------------------------- - -IDEAL_PD_ACTUATORS = { - "legs": IdealPDActuatorCfg( - joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], - stiffness=40.0, - damping=5.0, - effort_limit=80.0, - ), -} - -DC_MOTOR_ACTUATORS = { - "legs": DCMotorCfg( - joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], - saturation_effort=120.0, - effort_limit=80.0, - velocity_limit=7.5, - stiffness={".*": 40.0}, - damping={".*": 5.0}, - ), -} - -MIXED_ACTUATORS = { - "hips": IdealPDActuatorCfg( - joint_names_expr=[".*HAA"], - stiffness=40.0, - damping=5.0, - effort_limit=80.0, - ), - "knees": DCMotorCfg( - joint_names_expr=[".*HFE", ".*KFE"], - saturation_effort=120.0, - effort_limit=80.0, - velocity_limit=7.5, - stiffness={".*": 40.0}, - damping={".*": 5.0}, - ), -} - # --------------------------------------------------------------------------- # Simulation runner # --------------------------------------------------------------------------- @@ -127,8 +99,8 @@ def _run_simulation( ) -> dict: """Run ANYmal-C and return recorded trajectories + telemetry. - Always records ``joint_pos``, ``joint_vel``, ``computed_torque``, and - ``applied_torque`` so callers don't need a separate "with telemetry" + Always records ``joint_pos``, ``joint_vel``, ``computed_effort``, and + ``applied_effort`` so callers don't need a separate "with telemetry" runner. Optionally applies a constant per-DOF feedforward effort target. Args: @@ -216,8 +188,8 @@ def _run_simulation( articulation.set_joint_effort_target_index(target=effort_target) recorded_pos, recorded_vel = [], [] - recorded_computed, recorded_applied = [], [] - recorded_adapter_computed, recorded_adapter_applied = [], [] + recorded_computed_effort, recorded_applied_effort = [], [] + recorded_adapter_applied = [] for _ in range(num_steps): if handles_dec: articulation.write_data_to_sim() @@ -230,10 +202,9 @@ def _run_simulation( articulation.update(dt) recorded_pos.append(wp.to_torch(articulation.data.joint_pos).clone()) recorded_vel.append(wp.to_torch(articulation.data.joint_vel).clone()) - recorded_computed.append(wp.to_torch(articulation.data.computed_torque).clone()) - recorded_applied.append(wp.to_torch(articulation.data.applied_torque).clone()) + recorded_computed_effort.append(articulation.actuators.computed_effort.torch.clone()) + recorded_applied_effort.append(articulation.actuators.applied_effort.torch.clone()) if use_newton_actuators: - recorded_adapter_computed.append(wp.to_torch(articulation.data._sim_bind_joint_computed_effort).clone()) recorded_adapter_applied.append(wp.to_torch(articulation.data._sim_bind_joint_effort).clone()) return { @@ -243,9 +214,8 @@ def _run_simulation( "adapter_joint_names": backend_joint_names, "joint_pos": recorded_pos, "joint_vel": recorded_vel, - "computed_torque": recorded_computed, - "applied_torque": recorded_applied, - "adapter_computed_effort": recorded_adapter_computed, + "computed_effort": recorded_computed_effort, + "applied_effort": recorded_applied_effort, "adapter_applied_effort": recorded_adapter_applied, "target_pos": target_pos.clone(), "target_vel": target_vel.clone(), @@ -279,12 +249,13 @@ def test_newton_actuator_rollout_matches_reversed_joint_ordering() -> None: # --------------------------------------------------------------------------- -class _EquivalenceTestBase(unittest.TestCase): +class _EquivalenceTestBase(EquivalenceAssertionsMixin, unittest.TestCase): """Base for Lab-vs-Newton equivalence tests. Subclasses set ``actuators`` to the config under test. ``setUpClass`` runs the simulation with both ``use_newton_actuators=False`` (Lab path) - and ``True`` (Newton path) and stores the results. + and ``True`` (Newton path) and stores the results. The ``test_*_match`` + oracles come from :class:`EquivalenceAssertionsMixin`. """ __test__ = False @@ -294,12 +265,6 @@ class _EquivalenceTestBase(unittest.TestCase): newton_cfg: NewtonCfg = NEWTON_CFG num_steps: int = NUM_STEPS decimation: int = 1 - pos_atol: float = 2e-3 - pos_rtol: float = 1e-3 - vel_atol: float = 1e-2 - vel_rtol: float = 1e-2 - torque_atol: float = 1e-3 - torque_rtol: float = 1e-3 @classmethod def setUpClass(cls): @@ -313,50 +278,6 @@ def setUpClass(cls): cls.lab_result = _run_simulation(cls.actuators, use_newton_actuators=False, **kwargs) cls.newton_result = _run_simulation(cls.actuators, use_newton_actuators=True, **kwargs) - def test_joint_positions_match(self): - for step_i, (lab, newton) in enumerate(zip(self.lab_result["joint_pos"], self.newton_result["joint_pos"])): - torch.testing.assert_close( - lab, - newton, - atol=self.pos_atol, - rtol=self.pos_rtol, - msg=f"Joint positions diverged at step {step_i}", - ) - - def test_joint_velocities_match(self): - for step_i, (lab, newton) in enumerate(zip(self.lab_result["joint_vel"], self.newton_result["joint_vel"])): - torch.testing.assert_close( - lab, - newton, - atol=self.vel_atol, - rtol=self.vel_rtol, - msg=f"Joint velocities diverged at step {step_i}", - ) - - def test_applied_torque_match(self): - for step_i, (lab, newton) in enumerate( - zip(self.lab_result["applied_torque"], self.newton_result["applied_torque"]) - ): - torch.testing.assert_close( - lab, - newton, - atol=self.torque_atol, - rtol=self.torque_rtol, - msg=f"applied_torque diverged at step {step_i}", - ) - - def test_computed_torque_match(self): - for step_i, (lab, newton) in enumerate( - zip(self.lab_result["computed_torque"], self.newton_result["computed_torque"]) - ): - torch.testing.assert_close( - lab, - newton, - atol=self.torque_atol, - rtol=self.torque_rtol, - msg=f"computed_torque diverged at step {step_i}", - ) - # --------------------------------------------------------------------------- # Equivalence tests with different actuator types @@ -377,36 +298,6 @@ class TestDCMotorEquivalence(_EquivalenceTestBase): actuators = DC_MOTOR_ACTUATORS -class TestMixedActuatorEquivalence(_EquivalenceTestBase): - """Mixed actuators (IdealPD on HAA, DCMotor on HFE/KFE): Lab vs Newton.""" - - __test__ = True - actuators = MIXED_ACTUATORS - - -MIXED_WITH_IMPLICIT_ACTUATORS = { - "hips": ImplicitActuatorCfg( - joint_names_expr=[".*HAA"], - stiffness=40.0, - damping=5.0, - ), - "thighs": IdealPDActuatorCfg( - joint_names_expr=[".*HFE"], - stiffness=40.0, - damping=5.0, - effort_limit=80.0, - ), - "knees": DCMotorCfg( - joint_names_expr=[".*KFE"], - saturation_effort=120.0, - effort_limit=80.0, - velocity_limit=7.5, - stiffness=40.0, - damping=5.0, - ), -} - - class TestMixedWithImplicitEquivalence(_EquivalenceTestBase): """Implicit HAA + IdealPD HFE + DCMotor KFE: Lab vs Newton. @@ -418,26 +309,6 @@ class TestMixedWithImplicitEquivalence(_EquivalenceTestBase): actuators = MIXED_WITH_IMPLICIT_ACTUATORS -# --------------------------------------------------------------------------- -# Implicit-only fast-path: enable Newton actuator branch with no explicit groups -# --------------------------------------------------------------------------- - -IMPLICIT_ONLY_ACTUATORS = { - "legs": ImplicitActuatorCfg( - joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], - stiffness=40.0, - damping=5.0, - ), -} - - -class TestImplicitOnlyEquivalence(_EquivalenceTestBase): - """All-implicit articulation with ``use_newton_actuators=True``: Lab vs fast-path.""" - - __test__ = True - actuators = IMPLICIT_ONLY_ACTUATORS - - # --------------------------------------------------------------------------- # Implicit + non-zero feedforward effort target # --------------------------------------------------------------------------- @@ -461,16 +332,6 @@ class TestImplicitWithFeedforwardEquivalence(_EquivalenceTestBase): # --------------------------------------------------------------------------- -CARTPOLE_EXPLICIT_ACTUATORS = { - "all_joints": IdealPDActuatorCfg( - joint_names_expr=["slider_to_cart", "cart_to_pole"], - stiffness=10.0, - damping=1.0, - effort_limit=100.0, - ), -} - - def _run_anymal_and_cartpole(use_newton_actuators: bool, *, num_steps: int = NUM_STEPS) -> dict: """Spawn ANYmal-C + Cartpole per env (different DOF counts, different base types).""" from isaaclab_assets import CARTPOLE_CFG # noqa: PLC0415 @@ -567,91 +428,19 @@ def test_cartpole_matches_lab(self): # --------------------------------------------------------------------------- -class _MockScene: - """Minimal stand-in for ``InteractiveScene`` accepted by ``ManagerTermBase``.""" - - def __init__(self, assets: dict, num_envs: int): - self._assets = assets - self.num_envs = num_envs - - def __getitem__(self, name: str): - return self._assets[name] - - -class _MockEnv: - """Minimal stand-in for ``ManagerBasedEnv`` for invoking DR terms. - - ``randomize_actuator_gains`` only reads ``env.scene[name]`` and - ``env.scene.num_envs`` (plus ``env.num_envs`` / ``env.device`` from the - ``ManagerTermBase`` properties). No simulator access is needed because - the DR term reaches the actuator adapter via ``self.asset.newton_actuator_adapter``. - """ - - def __init__(self, assets: dict, num_envs: int, device: str): - self.scene = _MockScene(assets, num_envs) - self.num_envs = num_envs - self.device = device - - -def _build_dr_term(env, asset_name, joint_ids=None): - from isaaclab.envs.mdp.events import randomize_actuator_gains # noqa: PLC0415 - from isaaclab.managers import EventTermCfg, SceneEntityCfg # noqa: PLC0415 - - asset_cfg = SceneEntityCfg(asset_name) - if joint_ids is not None: - asset_cfg.joint_ids = joint_ids - cfg = EventTermCfg( - func=randomize_actuator_gains, - params={ - "asset_cfg": asset_cfg, - "stiffness_distribution_params": (100.0, 100.0), - "damping_distribution_params": (5.0, 5.0), - "operation": "abs", - "distribution": "uniform", - }, - ) - return randomize_actuator_gains(cfg, env), asset_cfg - - class TestRandomizeActuatorGainsViaEventsNewton(unittest.TestCase): """End-to-end DR test for the Newton backend. - Drives ``randomize_actuator_gains`` (events.py) and verifies the new - kp/kd values land on the controllers of the articulation's Newton - actuators — exercising the full path: events → - ``write_actuator_stiffness_to_sim`` → per-actuator - ``ArticulationView.set_actuator_parameter`` (with the per-DOF mapping - silently skipping actuators that belong to other articulations). + Drives ``randomize_actuator_gains`` and verifies that kp/kd values reach + the controllers of the articulation's Newton actuators through + ``write_group_parameter``; the assertions read the + controllers back via the public ``read_group_parameter``. With ``operation="abs"`` and ``distribution="uniform"`` over a degenerate range ``(K, K)``, every randomized cell is set to exactly ``K`` — so the assertions are deterministic. """ - @staticmethod - def _gather_param(articulation, attr) -> torch.Tensor: - """Read ``controller.`` for every Newton actuator via the view. - - Iterates the global adapter's actuator list. ``get_actuator_parameter`` - returns zeros for DOFs that don't belong to this articulation's - view (the per-DOF mapping skips them), so summing across all - actuators yields a clean ``(num_envs, num_joints)`` snapshot for - this articulation. - """ - n_env = articulation.num_instances - n_j = articulation.num_joints - out = torch.zeros((n_env, n_j), device=articulation.device) - adapter = SimulationManager._adapter - if adapter is None: - return out - for act in adapter.actuators: - ctrl = act.controller - if not hasattr(ctrl, attr): - continue - cur_wp = articulation._root_view.get_actuator_parameter(act, ctrl, attr) - out += wp.to_torch(cur_wp) - return out - def test_single_articulation(self): sim_cfg = SimulationCfg(dt=DT, physics=NEWTON_CFG, use_newton_actuators=True) with build_simulation_context( @@ -672,11 +461,23 @@ def test_single_articulation(self): adapter = SimulationManager._adapter self.assertIsNotNone(adapter, "Newton adapter should exist with use_newton_actuators=True") - kp_before = self._gather_param(anymal, "kp").clone() - kd_before = self._gather_param(anymal, "kd").clone() - - env = _MockEnv({"robot": anymal}, NUM_ENVS, anymal.device) - term, asset_cfg = _build_dr_term(env, "robot") + read = functools.partial(read_group_parameter, anymal.actuators) + n = anymal.num_joints + # Before DR, native gain reads must return the configured values for + # *every* env. IDEAL_PD_ACTUATORS covers all 12 joints with constant + # gains, so every cell of both env rows must equal the configured + # value. This is also the regression check for the env-major DOF + # stride decoding on floating-base articulations (ANYmal-C has 6 + # free-root DOFs + 12 joints -> a per-env stride of 18 vs. + # ``num_joints == 12``): a wrong stride corrupts every env past the + # first. + legs_stiffness_before = read("legs", "controller", "kp").clone() + legs_damping_before = read("legs", "controller", "kd").clone() + torch.testing.assert_close(legs_stiffness_before, torch.full((NUM_ENVS, n), 40.0, device=anymal.device)) + torch.testing.assert_close(legs_damping_before, torch.full((NUM_ENVS, n), 5.0, device=anymal.device)) + + env = MockEnv({"robot": anymal}, NUM_ENVS, anymal.device) + term, asset_cfg = build_dr_term(env, "robot") env_ids = torch.tensor([0], device=anymal.device, dtype=torch.long) term( @@ -689,15 +490,15 @@ def test_single_articulation(self): distribution="uniform", ) - kp_after = self._gather_param(anymal, "kp") - kd_after = self._gather_param(anymal, "kd") - n = anymal.num_joints - torch.testing.assert_close(kp_after[0], torch.full((n,), 100.0, device=anymal.device)) - torch.testing.assert_close(kd_after[0], torch.full((n,), 5.0, device=anymal.device)) + # Named native-group reads project the controller values immediately. + torch.testing.assert_close( + read("legs", "controller", "kp")[0], torch.full((n,), 100.0, device=anymal.device) + ) + torch.testing.assert_close(read("legs", "controller", "kd")[0], torch.full((n,), 5.0, device=anymal.device)) # Other envs untouched. for env_idx in range(1, NUM_ENVS): - torch.testing.assert_close(kp_after[env_idx], kp_before[env_idx]) - torch.testing.assert_close(kd_after[env_idx], kd_before[env_idx]) + torch.testing.assert_close(read("legs", "controller", "kp")[env_idx], legs_stiffness_before[env_idx]) + torch.testing.assert_close(read("legs", "controller", "kd")[env_idx], legs_damping_before[env_idx]) def test_two_articulations(self): from isaaclab_assets import CARTPOLE_CFG # noqa: PLC0415 @@ -725,13 +526,15 @@ def test_two_articulations(self): self.assertIsNotNone(SimulationManager._adapter) - anymal_kp_before = self._gather_param(anymal, "kp").clone() - anymal_kd_before = self._gather_param(anymal, "kd").clone() - cp_kp_before = self._gather_param(cartpole, "kp").clone() - cp_kd_before = self._gather_param(cartpole, "kd").clone() + anymal_read = functools.partial(read_group_parameter, anymal.actuators) + cartpole_read = functools.partial(read_group_parameter, cartpole.actuators) + anymal_stiffness_before = anymal_read("legs", "controller", "kp").clone() + anymal_damping_before = anymal_read("legs", "controller", "kd").clone() + cartpole_stiffness_before = cartpole_read("all_joints", "controller", "kp").clone() + cartpole_damping_before = cartpole_read("all_joints", "controller", "kd").clone() - env = _MockEnv({"anymal": anymal, "cartpole": cartpole}, NUM_ENVS, anymal.device) - term, asset_cfg = _build_dr_term(env, "cartpole") + env = MockEnv({"anymal": anymal, "cartpole": cartpole}, NUM_ENVS, anymal.device) + term, asset_cfg = build_dr_term(env, "cartpole") env_ids = torch.tensor([0], device=anymal.device, dtype=torch.long) term( @@ -744,95 +547,32 @@ def test_two_articulations(self): distribution="uniform", ) - cp_kp_after = self._gather_param(cartpole, "kp") - cp_kd_after = self._gather_param(cartpole, "kd") n_cp = cartpole.num_joints - torch.testing.assert_close(cp_kp_after[0], torch.full((n_cp,), 100.0, device=anymal.device)) - torch.testing.assert_close(cp_kd_after[0], torch.full((n_cp,), 5.0, device=anymal.device)) + torch.testing.assert_close( + cartpole_read("all_joints", "controller", "kp")[0], torch.full((n_cp,), 100.0, device=anymal.device) + ) + torch.testing.assert_close( + cartpole_read("all_joints", "controller", "kd")[0], torch.full((n_cp,), 5.0, device=anymal.device) + ) # ANYmal is untouched (DR was scoped to cartpole). - torch.testing.assert_close(self._gather_param(anymal, "kp"), anymal_kp_before) - torch.testing.assert_close(self._gather_param(anymal, "kd"), anymal_kd_before) + torch.testing.assert_close(anymal_read("legs", "controller", "kp"), anymal_stiffness_before) + torch.testing.assert_close(anymal_read("legs", "controller", "kd"), anymal_damping_before) # Cartpole's other envs are also untouched (env_ids=[0] only). for env_idx in range(1, NUM_ENVS): - torch.testing.assert_close(cp_kp_after[env_idx], cp_kp_before[env_idx]) - torch.testing.assert_close(cp_kd_after[env_idx], cp_kd_before[env_idx]) - - -class TestNewtonActuatorGainSnapshotEnvStride(unittest.TestCase): - """Regression: the init-time kp/kd snapshot must be correct for every env. - - ``build_newton_actuator_defaults`` scatters each Newton actuator's - ``controller.kp`` / ``controller.kd`` into a per-articulation - ``(num_envs, num_joints)`` tensor (``newton_default_stiffness`` / - ``newton_default_damping``), which ``randomize_actuator_gains`` reads as - its DR baseline. On a floating-base articulation the actuator ``indices`` - are laid out env-major with a per-env stride equal to the *whole model's* - per-env DOF count (free-root DOFs + joints), which exceeds - ``articulation.num_joints``. If the scatter decodes the env with - ``num_joints`` instead of that stride, env 1's DOFs alias to the wrong - rows (and partly out of bounds), corrupting the snapshot for every env - past the first. - - ANYmal-C is floating base (6 free-root DOFs + 12 actuated joints -> a - per-env stride of 18 vs. ``num_joints == 12``), so the bug manifests here - with ``NUM_ENVS == 2``: without the fix, ``newton_default_stiffness[1]`` - is not uniformly the configured gain (its leading entries stay zero, as - they are never written). - """ - - def test_snapshot_matches_config_for_all_envs(self): - sim_cfg = SimulationCfg(dt=DT, physics=NEWTON_CFG, use_newton_actuators=True) - with build_simulation_context( - device="cuda:0", - gravity_enabled=True, - add_ground_plane=True, - sim_cfg=sim_cfg, - ) as sim: - sim._app_control_on_stop_handle = None - for i in range(NUM_ENVS): - sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) - art_cfg = ANYMAL_C_CFG.replace( - actuators=IDEAL_PD_ACTUATORS, - prim_path="/World/Env_[^/]*/Robot", - ) - anymal = Articulation(art_cfg) - sim.reset() - assert anymal.is_initialized - - stiffness = anymal.newton_default_stiffness - damping = anymal.newton_default_damping - self.assertIsNotNone(stiffness, "expected a Newton kp snapshot with use_newton_actuators=True") - self.assertIsNotNone(damping, "expected a Newton kd snapshot with use_newton_actuators=True") - - n_j = anymal.num_joints - self.assertEqual(tuple(stiffness.shape), (NUM_ENVS, n_j)) - self.assertEqual(tuple(damping.shape), (NUM_ENVS, n_j)) - - # IDEAL_PD_ACTUATORS covers all 12 joints with constant gains, so - # every cell of both env rows must equal the configured value. - expected_kp = torch.full((NUM_ENVS, n_j), 40.0, device=anymal.device) - expected_kd = torch.full((NUM_ENVS, n_j), 5.0, device=anymal.device) - torch.testing.assert_close(stiffness, expected_kp) - torch.testing.assert_close(damping, expected_kd) + torch.testing.assert_close( + cartpole_read("all_joints", "controller", "kp")[env_idx], cartpole_stiffness_before[env_idx] + ) + torch.testing.assert_close( + cartpole_read("all_joints", "controller", "kd")[env_idx], cartpole_damping_before[env_idx] + ) # --------------------------------------------------------------------------- # DelayedPD equivalence: PD with actuator command delay # --------------------------------------------------------------------------- -DELAYED_PD_ACTUATORS = { - "legs": DelayedPDActuatorCfg( - joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], - stiffness=40.0, - damping=5.0, - effort_limit=80.0, - min_delay=2, - max_delay=4, - ), -} - class TestDelayedPDEquivalence(_EquivalenceTestBase): """DelayedPDActuator on all 12 joints: Lab vs Newton. @@ -894,268 +634,57 @@ class TestDecimationDCMotor(_DecimationMixin, TestDCMotorEquivalence): """DCMotor — same equivalence checks, with decimation=2 + CUDA graph.""" -class TestDecimationIdealPD(_DecimationMixin, TestIdealPDEquivalence): - """IdealPD — decimation=2 + CUDA graph.""" - - class TestDecimationDelayedPD(_DecimationMixin, TestDelayedPDEquivalence): """DelayedPD — decimation=2 + CUDA graph (delay queue stepped inside the captured graph).""" -class TestDecimationMixed(_DecimationMixin, TestMixedActuatorEquivalence): - """Mixed (IdealPD + DCMotor) — decimation=2 + CUDA graph.""" - - # --------------------------------------------------------------------------- # Per-env reset: actuator state isolation # --------------------------------------------------------------------------- -RESET_WARMUP_STEPS = 3 - -class TestActuatorStateReset(unittest.TestCase): - """Reset must clear the actuator state buffers for the requested envs only. +class TestActuatorStateReset(ActuatorStateResetBase, unittest.TestCase): + """Per-env actuator state reset isolation on the Newton backend. - Inspects ``adapter.actuators[i].state.delay_state.num_pushes`` directly: - - * After warmup, ``num_pushes > 0`` for every DOF (buffer was populated). - * After ``articulation.reset(env_ids=[0])``, the entries for env 0's DOFs - must be ``0`` and the entries for env 1's DOFs must remain ``> 0``. - - Done independently on Lab and Newton paths. Lab inspects the - ``positions_delay_buffer._circular_buffer`` of its DelayedPDActuator; - Newton inspects the model-wide adapter's per-actuator state. + The scenario and assertions live in :class:`ActuatorStateResetBase`; + this subclass provides the Newton sim config and the model-wide adapter. """ - RESET_ENV: int = 0 - UNCHANGED_ENV: int = 1 + def _make_sim_cfg(self, use_newton_actuators: bool) -> SimulationCfg: + return SimulationCfg(dt=DT, physics=NEWTON_CFG, use_newton_actuators=use_newton_actuators) - def _build_and_warm(self, *, use_newton_actuators: bool): - sim_cfg = SimulationCfg( - dt=DT, - physics=NEWTON_CFG, - use_newton_actuators=use_newton_actuators, - ) - ctx = build_simulation_context( - device="cuda:0", - gravity_enabled=True, - add_ground_plane=True, - sim_cfg=sim_cfg, - ) - sim = ctx.__enter__() - sim._app_control_on_stop_handle = None - for i in range(NUM_ENVS): - sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) - art_cfg = ANYMAL_C_CFG.replace( - actuators=DELAYED_PD_ACTUATORS, - prim_path="/World/Env_[^/]*/Robot", - ) - articulation = Articulation(art_cfg) - sim.reset() + def _make_articulation(self) -> Articulation: + return Articulation(ANYMAL_C_CFG.replace(actuators=DELAYED_PD_ACTUATORS, prim_path="/World/Env_.*/Robot")) - init_pos = wp.to_torch(articulation.data.joint_pos).clone() - target_pos = init_pos + TARGET_OFFSET - target_vel = torch.zeros_like(init_pos) - articulation.set_joint_position_target_index(target=target_pos) - articulation.set_joint_velocity_target_index(target=target_vel) - for _ in range(RESET_WARMUP_STEPS): - articulation.write_data_to_sim() - sim.step() - articulation.update(DT) - return ctx, sim, articulation - - def test_newton_state_reset_isolated_to_reset_env(self): - """Newton: ``num_pushes`` zeroes for env 0's DOFs only after reset of [0].""" - ctx, sim, articulation = self._build_and_warm(use_newton_actuators=True) - try: - adapter = SimulationManager._adapter - self.assertIsNotNone(adapter) - # Find a DelayedPD actuator (it's the only one with delay_state). - stateful_pairs = [ - (act, st) - for act, st in zip(adapter.actuators, adapter._states_a) - if st is not None and getattr(st, "delay_state", None) is not None - ] - self.assertGreater(len(stateful_pairs), 0, "expected at least one DelayedPD actuator with delay_state") - - # Per-DOF entry layout inside each actuator's state: ``act.indices`` - # is the flat global DOF id; envs are stacked so env 0's DOFs come first. - for act, state in stateful_pairs: - pushes_before = state.delay_state.num_pushes.numpy() - self.assertTrue( - (pushes_before > 0).all(), - "expected non-zero num_pushes for all DOFs after warmup", - ) - - articulation.reset(env_ids=torch.tensor([self.RESET_ENV], device=articulation.device, dtype=torch.long)) - - # Map each entry of ``act.indices`` to its env via the adapter's full - # per-env DOF count (model.joint_dof_count // num_envs — includes free - # joint DOFs on floating-base articulations, unlike articulation.num_joints - # which counts only actuated DOFs). - for act, state in stateful_pairs: - pushes_after = state.delay_state.num_pushes.numpy() - indices_np = act.indices.numpy() - for i, global_dof in enumerate(indices_np): - env = int(global_dof) // adapter.num_joints - if env == self.RESET_ENV: - self.assertEqual( - int(pushes_after[i]), - 0, - f"DOF {i} (env {env}) should be reset to 0, got {pushes_after[i]}", - ) - else: - self.assertGreater( - int(pushes_after[i]), - 0, - f"DOF {i} (env {env}) was NOT in reset env_ids but num_pushes is 0", - ) - finally: - ctx.__exit__(None, None, None) - - def test_lab_state_reset_isolated_to_reset_env(self): - """Lab: DelayedPDActuator circular buffer zeroed for env 0 only.""" - ctx, sim, articulation = self._build_and_warm(use_newton_actuators=False) - try: - from isaaclab.actuators import DelayedPDActuator # noqa: PLC0415 - - delayed = [a for a in articulation.actuators.values() if isinstance(a, DelayedPDActuator)] - self.assertGreater(len(delayed), 0, "expected at least one Lab DelayedPDActuator") - actuator = delayed[0] - buf = actuator.positions_delay_buffer._circular_buffer._buffer - # ``_buffer`` shape: (max_length, batch_size, num_joints). - self.assertIsNotNone(buf, "delay buffer should be populated after warmup") - self.assertTrue( - (buf[:, self.UNCHANGED_ENV] != 0).any().item(), - "expected non-zero buffer entries for env 1 after warmup", - ) - - articulation.reset(env_ids=torch.tensor([self.RESET_ENV], device=articulation.device, dtype=torch.long)) - - self.assertTrue( - torch.all(buf[:, self.RESET_ENV] == 0).item(), - f"Lab: env {self.RESET_ENV} buffer not zeroed after reset.", - ) - self.assertTrue( - (buf[:, self.UNCHANGED_ENV] != 0).any().item(), - f"Lab: env {self.UNCHANGED_ENV} buffer was zeroed — reset leaked into an unselected env.", - ) - finally: - ctx.__exit__(None, None, None) + def _get_adapter(self, articulation): + return SimulationManager._adapter # --------------------------------------------------------------------------- # RemotizedPD actuator: PD + delay + position-based clamping lookup table # --------------------------------------------------------------------------- -SPOT_KNEE_LOOKUP = [ - [-2.792900, -24.776718, 37.165077], - [-2.767442, -26.290108, 39.435162], - [-2.741984, -27.793369, 41.690054], - [-2.716526, -29.285997, 43.928996], - [-2.691068, -30.767536, 46.151304], - [-2.665610, -32.237423, 48.356134], - [-2.640152, -33.695168, 50.542751], - [-2.614694, -35.140221, 52.710331], - [-2.589236, -36.572052, 54.858078], - [-2.563778, -37.990086, 56.985128], - [-2.538320, -39.393730, 59.090595], - [-2.512862, -40.782406, 61.173609], - [-2.487404, -42.155487, 63.233231], - [-2.461946, -43.512371, 65.268557], - [-2.436488, -44.852371, 67.278557], - [-2.411030, -46.174873, 69.262310], - [-2.385572, -47.479156, 71.218735], - [-2.360114, -48.764549, 73.146824], - [-2.334656, -50.030334, 75.045502], - [-2.309198, -51.275761, 76.913641], - [-2.283740, -52.500103, 78.750154], - [-2.258282, -53.702587, 80.553881], - [-2.232824, -54.882442, 82.323664], - [-2.207366, -56.038860, 84.058290], - [-2.181908, -57.171028, 85.756542], - [-2.156450, -58.278133, 87.417200], - [-2.130992, -59.359314, 89.038971], - [-2.105534, -60.413738, 90.620607], - [-2.080076, -61.440529, 92.160793], - [-2.054618, -62.438812, 93.658218], - [-2.029160, -63.407692, 95.111538], - [-2.003702, -64.346268, 96.519402], - [-1.978244, -65.253670, 97.880505], - [-1.952786, -66.128944, 99.193417], - [-1.927328, -66.971176, 100.456764], - [-1.901870, -67.779457, 101.669186], - [-1.876412, -68.552864, 102.829296], - [-1.850954, -69.290451, 103.935677], - [-1.825496, -69.991325, 104.986988], - [-1.800038, -70.654541, 105.981812], - [-1.774580, -71.279190, 106.918785], - [-1.749122, -71.864319, 107.796478], - [-1.723664, -72.409088, 108.613632], - [-1.698206, -72.912567, 109.368851], - [-1.672748, -73.373871, 110.060806], - [-1.647290, -73.792130, 110.688194], - [-1.621832, -74.166512, 111.249767], - [-1.596374, -74.496147, 111.744221], - [-1.570916, -74.780251, 112.170376], - [-1.545458, -75.017998, 112.526997], - [-1.520000, -75.208656, 112.812984], - [-1.494542, -75.351448, 113.027172], - [-1.469084, -75.445686, 113.168530], - [-1.443626, -75.490677, 113.236015], - [-1.418168, -75.485771, 113.228657], - [-1.392710, -75.430344, 113.145515], - [-1.367252, -75.323830, 112.985744], - [-1.341794, -75.165688, 112.748531], - [-1.316336, -74.955406, 112.433109], - [-1.290878, -74.692551, 112.038826], - [-1.265420, -74.376694, 111.565041], - [-1.239962, -74.007477, 111.011215], - [-1.214504, -73.584579, 110.376869], - [-1.189046, -73.107742, 109.661613], - [-1.163588, -72.576752, 108.865128], - [-1.138130, -71.991455, 107.987183], - [-1.112672, -71.351707, 107.027561], - [-1.087214, -70.657486, 105.986229], - [-1.061756, -69.908813, 104.863220], - [-1.036298, -69.105721, 103.658581], - [-1.010840, -68.248337, 102.372505], - [-0.985382, -67.336861, 101.005291], - [-0.959924, -66.371513, 99.557270], - [-0.934466, -65.352615, 98.028923], - [-0.909008, -64.280533, 96.420799], - [-0.883550, -63.155693, 94.733540], - [-0.858092, -61.978588, 92.967882], - [-0.832634, -60.749775, 91.124662], - [-0.807176, -59.469845, 89.204767], - [-0.781718, -58.139503, 87.209255], - [-0.756260, -56.759487, 85.139231], - [-0.730802, -55.330616, 82.995924], - [-0.705344, -53.853729, 80.780594], - [-0.679886, -52.329796, 78.494694], - [-0.654428, -50.759762, 76.139643], - [-0.628970, -49.144699, 73.717049], - [-0.603512, -47.485737, 71.228605], - [-0.578054, -45.784004, 68.676006], - [-0.552596, -44.040764, 66.061146], - [-0.527138, -42.257267, 63.385900], - [-0.501680, -40.434883, 60.652325], - [-0.476222, -38.574947, 57.862421], - [-0.450764, -36.678982, 55.018473], - [-0.425306, -34.748432, 52.122648], - [-0.399848, -32.784836, 49.177254], - [-0.374390, -30.789810, 46.184715], - [-0.348932, -28.764952, 43.147428], - [-0.323474, -26.711969, 40.067954], - [-0.298016, -24.632576, 36.948864], - [-0.272558, -22.528547, 33.792821], - [-0.247100, -20.401667, 30.602500], -] -"""Spot knee joint parameter lookup table (102 entries). - -Columns: joint angle [rad], transmission ratio, output torque [N*m]. -Sourced from :mod:`isaaclab_assets.robots.spot`. -""" + +def _remotized_pd_actuators() -> dict: + """RemotizedPD (Spot knee lookup) on KFE with IdealPD on HAA/HFE.""" + from isaaclab.actuators.actuator_pd_cfg import RemotizedPDActuatorCfg # noqa: PLC0415 + + return { + "hips": IdealPDActuatorCfg( + joint_names_expr=[".*HAA", ".*HFE"], + stiffness=40.0, + damping=5.0, + actuator_effort_limit=80.0, + ), + "knees": RemotizedPDActuatorCfg( + joint_names_expr=[".*KFE"], + stiffness=60.0, + damping=1.5, + actuator_effort_limit=80.0, + max_delay=3, + joint_parameter_lookup=SPOT_KNEE_LOOKUP, + ), + } def _run_authoring_introspection(actuator_cfgs: dict) -> dict: @@ -1228,32 +757,13 @@ class TestRemotizedPDAuthoring(unittest.TestCase): """Verify RemotizedPDActuatorCfg is authored as Newton PD + delay + position-based clamping. - Uses the Spot knee lookup table (102 entries) on ANYmal's KFE joints, - with IdealPD on HAA and HFE joints. + Uses the Spot knee lookup table on ANYmal's KFE joints, with IdealPD + on HAA and HFE joints. """ @classmethod def setUpClass(cls): - from isaaclab.actuators.actuator_pd_cfg import RemotizedPDActuatorCfg # noqa: PLC0415 - - cls.result = _run_authoring_introspection( - { - "hips": IdealPDActuatorCfg( - joint_names_expr=[".*HAA", ".*HFE"], - stiffness=40.0, - damping=5.0, - effort_limit=80.0, - ), - "knees": RemotizedPDActuatorCfg( - joint_names_expr=[".*KFE"], - stiffness=60.0, - damping=1.5, - effort_limit=80.0, - max_delay=3, - joint_parameter_lookup=SPOT_KNEE_LOOKUP, - ), - } - ) + cls.result = _run_authoring_introspection(_remotized_pd_actuators()) def test_num_actuators(self): self.assertGreaterEqual(self.result["num_actuators"], 2) @@ -1281,24 +791,7 @@ class TestRemotizedPDEquivalence(_EquivalenceTestBase): @classmethod def setUpClass(cls): - from isaaclab.actuators.actuator_pd_cfg import RemotizedPDActuatorCfg # noqa: PLC0415 - - cls.actuators = { - "hips": IdealPDActuatorCfg( - joint_names_expr=[".*HAA", ".*HFE"], - stiffness=40.0, - damping=5.0, - effort_limit=80.0, - ), - "knees": RemotizedPDActuatorCfg( - joint_names_expr=[".*KFE"], - stiffness=60.0, - damping=1.5, - effort_limit=80.0, - max_delay=3, - joint_parameter_lookup=SPOT_KNEE_LOOKUP, - ), - } + cls.actuators = _remotized_pd_actuators() super().setUpClass() @@ -1311,72 +804,6 @@ class TestDecimationRemotizedPD(_DecimationMixin, TestRemotizedPDEquivalence): # --------------------------------------------------------------------------- -def _make_dummy_mlp_checkpoint(device: str = "cpu") -> str: - """Create a minimal TorchScript MLP checkpoint with metadata. - - The network accepts 6 inputs (3 history steps x 2 features per step - in pos_vel order) and outputs 1 effort. - """ - torch.manual_seed(42) - net = ( - torch.nn.Sequential( - torch.nn.Linear(6, 8), - torch.nn.ELU(), - torch.nn.Linear(8, 1), - ) - .to(device) - .eval() - ) - scripted = torch.jit.script(net) - - with tempfile.NamedTemporaryFile(suffix=".pt", delete=False) as tmp: - tmp_path = tmp.name - extra = { - "metadata.json": json.dumps( - { - "model_type": "mlp", - "input_order": "pos_vel", - "input_idx": [0, 1, 2], - "pos_scale": 1.0, - "vel_scale": 0.5, - "torque_scale": 2.0, - } - ) - } - torch.jit.save(scripted, tmp_path, _extra_files=extra) - return tmp_path - - -class _DummyLSTM(torch.nn.Module): - """Minimal LSTM network for actuator testing.""" - - def __init__(self): - super().__init__() - self.lstm = torch.nn.LSTM(input_size=2, hidden_size=4, num_layers=1, batch_first=True) - self.fc = torch.nn.Linear(4, 1) - - def forward( - self, - x: torch.Tensor, - hc: tuple[torch.Tensor, torch.Tensor], - ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]: - out, hc_new = self.lstm(x, hc) - return self.fc(out[:, -1, :]), hc_new - - -def _make_dummy_lstm_checkpoint(device: str = "cpu") -> str: - """Create a minimal TorchScript LSTM checkpoint with metadata.""" - torch.manual_seed(42) - net = _DummyLSTM().to(device).eval() - scripted = torch.jit.script(net) - - with tempfile.NamedTemporaryFile(suffix=".pt", delete=False) as tmp: - tmp_path = tmp.name - extra = {"metadata.json": json.dumps({"model_type": "lstm"})} - torch.jit.save(scripted, tmp_path, _extra_files=extra) - return tmp_path - - class TestNeuralMLPAuthoring(unittest.TestCase): """Verify ActuatorNetMLPCfg is authored as Newton NeuralMLP controller with DC motor clamping. @@ -1386,15 +813,15 @@ class TestNeuralMLPAuthoring(unittest.TestCase): def setUpClass(cls): from isaaclab.actuators.actuator_net_cfg import ActuatorNetMLPCfg # noqa: PLC0415 - cls.mlp_path = _make_dummy_mlp_checkpoint() + cls.mlp_path = make_dummy_mlp_checkpoint() cls.result = _run_authoring_introspection( { "mlp_legs": ActuatorNetMLPCfg( joint_names_expr=[".*HAA"], network_file=cls.mlp_path, saturation_effort=120.0, - effort_limit=80.0, - velocity_limit=7.5, + actuator_effort_limit=80.0, + actuator_velocity_limit=7.5, pos_scale=-1.0, vel_scale=1.0, torque_scale=1.0, @@ -1405,7 +832,7 @@ def setUpClass(cls): joint_names_expr=[".*HFE", ".*KFE"], stiffness=40.0, damping=5.0, - effort_limit=80.0, + actuator_effort_limit=80.0, ), } ) @@ -1436,21 +863,21 @@ class TestNeuralLSTMAuthoring(unittest.TestCase): def setUpClass(cls): from isaaclab.actuators.actuator_net_cfg import ActuatorNetLSTMCfg # noqa: PLC0415 - cls.lstm_path = _make_dummy_lstm_checkpoint() + cls.lstm_path = make_dummy_lstm_checkpoint() cls.result = _run_authoring_introspection( { "lstm_legs": ActuatorNetLSTMCfg( joint_names_expr=[".*HAA"], network_file=cls.lstm_path, saturation_effort=120.0, - effort_limit=80.0, - velocity_limit=7.5, + actuator_effort_limit=80.0, + actuator_velocity_limit=7.5, ), "pd_legs": IdealPDActuatorCfg( joint_names_expr=[".*HFE", ".*KFE"], stiffness=40.0, damping=5.0, - effort_limit=80.0, + actuator_effort_limit=80.0, ), } ) @@ -1556,55 +983,5 @@ def test_sync_torque_telemetry_keeps_user_order_effort_buffers_unmapped() -> Non np.testing.assert_allclose(applied.numpy(), np.asarray([[100.0, 200.0, 300.0]], dtype=np.float32)) -def test_newton_actuator_defaults_follow_requested_public_joint_order() -> None: - """Convert Newton actuator gain snapshots and managed IDs into public joint order.""" - from isaaclab_newton.actuators.adapter import build_newton_actuator_defaults - - controller = types.SimpleNamespace( - kp=wp.array((10.0, 30.0, 11.0, 31.0), dtype=wp.float32, device="cpu"), - kd=wp.array((1.0, 3.0, 1.1, 3.1), dtype=wp.float32, device="cpu"), - ) - actuator = types.SimpleNamespace( - controller=controller, - indices=wp.array((0, 2, 3, 5), dtype=wp.uint32, device="cpu"), - ) - - stiffness, damping, managed = build_newton_actuator_defaults( - actuators=[actuator], - num_envs=2, - num_joints=3, - dof_offset=0, - env_stride=3, - device="cpu", - joint_user_to_backend_indices=(2, 0, 1), - ) - - torch.testing.assert_close(stiffness, torch.tensor([[30.0, 10.0, 0.0], [31.0, 11.0, 0.0]])) - torch.testing.assert_close(damping, torch.tensor([[3.0, 1.0, 0.0], [3.1, 1.1, 0.0]])) - torch.testing.assert_close(managed, torch.tensor([0, 1], dtype=torch.int32)) - - -def test_newton_actuator_defaults_reject_incomplete_joint_permutation() -> None: - """Reject malformed actuator-default ordering maps with an actionable error.""" - from isaaclab_newton.actuators.adapter import build_newton_actuator_defaults - - with pytest.raises( - ValueError, - match=( - r"joint_user_to_backend_indices must contain each backend joint index exactly once; " - r"expected a permutation of 0\.\.2, got \(0, 0, 2\)\." - ), - ): - build_newton_actuator_defaults( - actuators=[], - num_envs=1, - num_joints=3, - dof_offset=0, - env_stride=3, - device="cpu", - joint_user_to_backend_indices=(0, 0, 2), - ) - - if __name__ == "__main__": unittest.main() diff --git a/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py b/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py index 01b7720e5e18..60941a2cc040 100644 --- a/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py +++ b/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py @@ -1003,20 +1003,42 @@ def test_clear_resets_rigid_body_force_capability(monkeypatch): assert manager._supports_rigid_body_force_input is False -def test_initialize_solver_prepares_picking_before_graph_capture(monkeypatch): - """Viewer force callbacks are registered after capability publication and before capture.""" +@pytest.mark.parametrize( + "native_path_active, native_graphable, expected_events", + [ + pytest.param(False, False, ["prepare", "capture"], id="lab_actuators"), + pytest.param(True, False, ["prepare", "capture"], id="native_non_graphable"), + pytest.param(True, True, ["prepare"], id="native_graphable"), + ], +) +def test_initialize_solver_prepares_picking_before_graph_capture( + monkeypatch, native_path_active, native_graphable, expected_events +): + """Viewer setup precedes initial capture, which only graphable native actuators defer.""" events: list[str] = [] sim_cfg = SimulationCfg( dt=1.0 / 120.0, - device="cuda:0", + device="cpu", physics=NewtonCfg(solver_cfg=MJWarpSolverCfg(), use_cuda_graph=False), ) with build_simulation_context(sim_cfg=sim_cfg) as sim: + build_solver = NewtonMJWarpManager._build_solver + + def build_solver_with_actuator_mode(cls, model, solver_cfg): + build_solver(model, solver_cfg) + NewtonManager._use_newton_actuators_active = native_path_active + NewtonManager._adapter = SimpleNamespace(is_all_graphable=native_graphable) + builder = sim.physics_manager.create_builder() body = builder.add_body(mass=1.0) builder.add_joint_revolute(parent=-1, child=body, axis=(0, 0, 1)) NewtonManager.set_builder(builder) + monkeypatch.setattr( + NewtonMJWarpManager, + "_build_solver", + classmethod(build_solver_with_actuator_mode), + ) monkeypatch.setattr(sim, "_prepare_newton_visualizer_for_capture", lambda: events.append("prepare")) monkeypatch.setattr( NewtonMJWarpManager, @@ -1026,7 +1048,7 @@ def test_initialize_solver_prepares_picking_before_graph_capture(monkeypatch): sim.reset() - assert events == ["prepare", "capture"] + assert events == expected_events def test_abstract_build_solver_raises(): diff --git a/source/isaaclab_newton/test/physics/test_newton_solver_reset.py b/source/isaaclab_newton/test/physics/test_newton_solver_reset.py index 01b221ad6ace..beafb4d45e53 100644 --- a/source/isaaclab_newton/test/physics/test_newton_solver_reset.py +++ b/source/isaaclab_newton/test/physics/test_newton_solver_reset.py @@ -40,8 +40,8 @@ def _generate_single_joint_articulations(num_articulations: int, device: str) -> actuators={ "joint": IdealPDActuatorCfg( joint_names_expr=[".*"], - effort_limit=400.0, - velocity_limit=100.0, + actuator_effort_limit=400.0, + actuator_velocity_limit=100.0, stiffness=0.0, damping=10.0, ), diff --git a/source/isaaclab_newton/test/sensors/test_joint_wrench_sensor.py b/source/isaaclab_newton/test/sensors/test_joint_wrench_sensor.py index 793ca32de440..b78c3ca65d0f 100644 --- a/source/isaaclab_newton/test/sensors/test_joint_wrench_sensor.py +++ b/source/isaaclab_newton/test/sensors/test_joint_wrench_sensor.py @@ -65,10 +65,10 @@ def _make_cartpole_articulation_cfg(pole_damping: float = 0.0) -> ArticulationCf ), actuators={ "cart_actuator": ImplicitActuatorCfg( - joint_names_expr=["slider_to_cart"], effort_limit_sim=400.0, stiffness=0.0, damping=10.0 + joint_names_expr=["slider_to_cart"], joint_effort_limit=400.0, stiffness=0.0, damping=10.0 ), "pole_actuator": ImplicitActuatorCfg( - joint_names_expr=["cart_to_pole"], effort_limit_sim=400.0, stiffness=0.0, damping=pole_damping + joint_names_expr=["cart_to_pole"], joint_effort_limit=400.0, stiffness=0.0, damping=pole_damping ), }, ) diff --git a/source/isaaclab_ov/changelog.d/actuator-collection.minor.rst b/source/isaaclab_ov/changelog.d/actuator-collection.minor.rst new file mode 100644 index 000000000000..4c10d591b6cb --- /dev/null +++ b/source/isaaclab_ov/changelog.d/actuator-collection.minor.rst @@ -0,0 +1,12 @@ +Changed +^^^^^^^ + +* Routed OVPhysX articulation actuator setup, compute, reset, and command + submission through :class:`~isaaclab.actuators.ActuatorCollection`. + +Added +^^^^^ + +* Added OVPhysX execution of supported native explicit actuators through the + shared host adapter when + :attr:`~isaaclab.sim.SimulationCfg.use_newton_actuators` is enabled. diff --git a/source/isaaclab_ov/isaaclab_ov/assets/articulation/actuator_control.py b/source/isaaclab_ov/isaaclab_ov/assets/articulation/actuator_control.py new file mode 100644 index 000000000000..163c08762d07 --- /dev/null +++ b/source/isaaclab_ov/isaaclab_ov/assets/articulation/actuator_control.py @@ -0,0 +1,202 @@ +# 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 + +"""OVPhysX actuator control adapter.""" + +from __future__ import annotations + +import logging +from collections.abc import Sequence + +import torch +import warp as wp + +from isaaclab.actuators import ActuatorCollection +from isaaclab.actuators.actuator_base_cfg import _is_implicit_actuator_cfg +from isaaclab.actuators.actuator_control import ArticulationActuatorControl +from isaaclab.actuators.newton.adapter import LightArticulationView, NewtonActuatorSelection +from isaaclab.actuators.newton.physx_runtime import PhysxActuatorRuntime +from isaaclab.assets.articulation import ordering_kernels +from isaaclab.sim.schemas.schemas_actuators import _validate_newton_native_actuator_cfgs +from isaaclab.sim.utils.queries import find_first_matching_prim +from isaaclab.sim.utils.stage import get_current_stage + +from isaaclab_ov import tensor_types as TT + +logger = logging.getLogger(__name__) + + +class OvPhysxActuatorControl(ArticulationActuatorControl): + """Actuator control adapter for the OVPhysX backend.""" + + def __init__(self, articulation): + super().__init__(articulation) + self._actuator_runtime = None + + @property + def _physx_actuator_wrapper(self): + """Expose the shared host wrapper for PhysX-native compatibility.""" + return None if self._actuator_runtime is None else self._actuator_runtime.wrapper + + def prepare_native_actuators(self, collection: ActuatorCollection, actuator_cfgs: dict) -> set[str]: + """Prepare optional Newton-native explicit actuators for OVPhysX.""" + articulation = self._articulation + articulation._physx_actuator_wrapper = None + articulation.newton_actuator_adapter = None + articulation._implicit_dof_mask = None + articulation._has_newton_actuators = False + self._actuator_runtime = None + + use_newton_actuators = getattr(articulation._sim_cfg, "use_newton_actuators", False) + if not use_newton_actuators: + return set() + + _validate_newton_native_actuator_cfgs(actuator_cfgs) + # Activate the runtime even without explicit native groups: implicit-only + # articulations still rely on it for the solver telemetry fast path. + self._native_actuator_path_active = True + articulation._has_newton_actuators = True + native_group_names = { + name for name, actuator_cfg in actuator_cfgs.items() if not _is_implicit_actuator_cfg(actuator_cfg) + } + first_prim = find_first_matching_prim(articulation.cfg.prim_path) + articulation_prim_path = str(first_prim.GetPath()) if first_prim is not None else None + self._actuator_runtime = PhysxActuatorRuntime(articulation, logger=logger) + self._actuator_runtime.prepare( + collection, + stage=get_current_stage(), + articulation_prim_path=articulation_prim_path, + adapt_usd_actuators=bool(native_group_names), + ) + articulation._physx_actuator_wrapper = self._actuator_runtime.wrapper + articulation.newton_actuator_adapter = self._actuator_runtime.adapter + return native_group_names + + def finalize_native_actuators(self, collection: ActuatorCollection) -> NewtonActuatorSelection | None: + if not self._native_actuator_path_active or self._actuator_runtime is None: + return None + self._actuator_runtime.finalize(collection) + adapter = self._actuator_runtime.adapter + if adapter is None: + return None + articulation = self._articulation + return NewtonActuatorSelection( + view=LightArticulationView(articulation.num_instances, articulation.num_joints, articulation.device), + actuators=adapter.actuators, + ) + + def compute_native_actuators(self, collection: ActuatorCollection, dt: float) -> bool: + if not self._native_actuator_path_active or self._actuator_runtime is None: + return False + # OVPhysX public-order state uses owned staging buffers, even for identity ordering. + # Refresh both fields before every controller step so it observes the current simulation state. + self._articulation._data._refresh_joint_pos() + self._articulation._data._refresh_joint_vel() + self._actuator_runtime.compute(collection, dt) + return True + + def reset_native_actuators(self, env_ids: Sequence[int] | slice) -> None: + if self._native_actuator_path_active and self._actuator_runtime is not None: + self._actuator_runtime.reset(env_ids) + + def stage_user_command( + self, + command_name: str, + collection: ActuatorCollection, + env_ids: torch.Tensor | wp.array | None, + joint_ids: torch.Tensor | wp.array | None, + env_mask: wp.array(dtype=wp.bool) | None, + joint_mask: wp.array(dtype=wp.bool) | None, + ) -> None: + """Stage a public-order user command in the corresponding OVPhysX binding.""" + tensor_type, can_write, user_buffer, backend_buffer = self._command_buffers(command_name, collection) + if not can_write: + return + articulation = self._articulation + target_backend = articulation._get_backend_ordered_joint_buffer(user_buffer, backend_buffer) + if env_mask is not None: + articulation._root_view.set_attribute(tensor_type, target_backend, mask=env_mask) + elif env_ids is not None: + articulation._root_view.set_attribute( + tensor_type, target_backend, indices=articulation._get_sim_env_ids(env_ids) + ) + else: + articulation._root_view.set_attribute(tensor_type, target_backend) + + def submit_commands(self, collection: ActuatorCollection) -> None: + articulation = self._articulation + # Native telemetry contains the local implicit-drive shadow. Submit raw runtime effort + # instead, so OVPhysX evaluates each implicit PD drive exactly once. + write_effort = articulation._can_write_effort + # position and velocity targets only for implicit actuators. + write_pos = articulation._has_implicit_actuators and articulation._can_write_pos_target + write_vel = articulation._has_implicit_actuators and articulation._can_write_vel_target + user_effort = collection._applied_effort + if self._actuator_runtime is not None: + user_effort = self._actuator_runtime.wrapper.joint_f_2d + if articulation.data.has_joint_ordering: + if write_effort or write_pos or write_vel: + ordering_kernels.launch_reorder_joint_targets_user_to_backend( + user_effort=user_effort, + user_pos_target=collection._joint_pos_target, + user_vel_target=collection._joint_vel_target, + backend_to_user=articulation.data.joint_ordering.backend_to_user, + write_effort=write_effort, + write_pos_target=write_pos, + write_vel_target=write_vel, + write_joint_act=False, + backend_effort=articulation._applied_effort_backend, + backend_pos_target=articulation._joint_pos_target_backend, + backend_vel_target=articulation._joint_vel_target_backend, + backend_joint_act=None, + device=self.device, + ) + effort = articulation._applied_effort_backend + pos_target = articulation._joint_pos_target_backend + vel_target = articulation._joint_vel_target_backend + else: + effort = user_effort + pos_target = collection._joint_pos_target + vel_target = collection._joint_vel_target + if write_effort: + articulation._root_view.set_attribute(TT.DOF_ACTUATION_FORCE, effort) + if write_pos: + articulation._root_view.set_attribute(TT.DOF_POSITION_TARGET, pos_target) + if write_vel: + articulation._root_view.set_attribute(TT.DOF_VELOCITY_TARGET, vel_target) + + def _command_buffers( + self, + command_name: str, + collection: ActuatorCollection, + ) -> tuple[ + TT.TensorType, + bool, + wp.array(dtype=wp.float32), + wp.array(dtype=wp.float32) | None, + ]: + articulation = self._articulation + if command_name == "position": + return ( + TT.DOF_POSITION_TARGET, + articulation._can_write_pos_target, + collection._joint_pos_target, + articulation._joint_pos_target_backend, + ) + if command_name == "velocity": + return ( + TT.DOF_VELOCITY_TARGET, + articulation._can_write_vel_target, + collection._joint_vel_target, + articulation._joint_vel_target_backend, + ) + if command_name == "effort": + return ( + TT.DOF_ACTUATION_FORCE, + articulation._can_write_effort, + collection._joint_effort_target, + articulation._joint_effort_target_backend, + ) + raise ValueError(f"Unsupported actuator command buffer '{command_name}'.") diff --git a/source/isaaclab_ov/isaaclab_ov/assets/articulation/articulation.py b/source/isaaclab_ov/isaaclab_ov/assets/articulation/articulation.py index b5290807e6ba..8e69ad1ab310 100644 --- a/source/isaaclab_ov/isaaclab_ov/assets/articulation/articulation.py +++ b/source/isaaclab_ov/isaaclab_ov/assets/articulation/articulation.py @@ -12,7 +12,6 @@ import re import warnings from collections.abc import Sequence -from typing import Any import numpy as np import torch @@ -21,6 +20,7 @@ from pxr import Usd, UsdPhysics import isaaclab.sim as sim_utils +from isaaclab.actuators import ActuatorCollection from isaaclab.assets.articulation import ordering_kernels from isaaclab.assets.articulation.articulation_cfg import ArticulationCfg from isaaclab.assets.articulation.base_articulation import BaseArticulation @@ -40,6 +40,7 @@ from isaaclab_ov.physics import OvPhysxManager from isaaclab_ov.sim.views.ovphysx_view import OvPhysxView +from .actuator_control import OvPhysxActuatorControl from .articulation_data import ArticulationData from .kernels import ( clamp_default_joint_pos_and_update_soft_limits_index_kernel, @@ -217,6 +218,9 @@ def reset( """ if (env_ids is None) or (env_ids == slice(None)): env_ids = slice(None) + # reset actuators, including backend-native actuator state. None selects all + # environments; delayed-actuator buffers do not accept a slice. + self.actuators.reset(None if env_ids == slice(None) else env_ids) # reset external wrenches. self._instantaneous_wrench_composer.reset(env_ids, env_mask) self._permanent_wrench_composer.reset(env_ids, env_mask) @@ -262,56 +266,9 @@ def write_data_to_sim(self) -> None: if inst.active: inst.reset() - # apply actuator models - self._apply_actuator_model() - # write actions into simulation (zeros are safe when no actuators are active). - # ``_applied_torque`` is the actuator-computed output (may differ from the raw - # commanded target, e.g. once clipped), so it must be reordered into its own - # scratch buffer rather than ``_joint_effort_target_backend``. The latter is the - # persistent mirror of the raw target that partial writes rely on for their - # unselected joints (see ``set_joint_effort_target_index``/``_mask``). - write_effort = self._can_write_effort - # position and velocity targets only for implicit actuators - write_pos = self._has_implicit_actuators and self._can_write_pos_target - write_vel = self._has_implicit_actuators and self._can_write_vel_target - if self.data.has_joint_ordering: - if write_effort or write_pos or write_vel: - # One fused gather replaces the per-target reorder launches. The - # fourth joint-acceleration output is disabled. - wp.launch( - ordering_kernels.reorder_joint_targets_user_to_backend, - dim=(self._num_instances, self._num_joints), - inputs=[ - self._data._applied_torque, - self._data._joint_pos_target, - self._data._joint_vel_target, - self.data.joint_ordering.backend_to_user, - write_effort, - write_pos, - write_vel, - False, - ], - outputs=[ - self._applied_torque_backend, - self._joint_pos_target_backend, - self._joint_vel_target_backend, - None, - ], - device=self._device, - ) - effort = self._applied_torque_backend - pos_target = self._joint_pos_target_backend - vel_target = self._joint_vel_target_backend - else: - effort = self._data._applied_torque - pos_target = self._data._joint_pos_target - vel_target = self._data._joint_vel_target - if write_effort: - self._root_view.set_attribute(TT.DOF_ACTUATION_FORCE, effort) - if write_pos: - self._root_view.set_attribute(TT.DOF_POSITION_TARGET, pos_target) - if write_vel: - self._root_view.set_attribute(TT.DOF_VELOCITY_TARGET, vel_target) + # apply actuator models and submit processed commands. + self.actuators.compute(OvPhysxManager.get_physics_dt()) + self.actuators.submit_commands() def update(self, dt: float) -> None: """Updates the simulation data. @@ -2716,302 +2673,6 @@ def set_inertias_mask( ) self._data._reset_dynamics(mass_matrix=True) - def _write_joint_target( - self, - target: torch.Tensor | wp.array, - *, - user_buffer: wp.array, - backend_buffer: wp.array | None, - tensor_type: TT.TensorType, - env_sel: Sequence[int] | torch.Tensor | wp.array | None, - joint_sel: Sequence[int] | torch.Tensor | wp.array | None, - use_mask: bool, - ) -> None: - """Write a joint target into the public buffer and push it to the backend binding. - - Shared implementation behind the six - ``set_joint_{position,velocity,effort}_target_{index,mask}`` setters. The public-order - target is always written to :paramref:`user_buffer`; under a non-identity joint ordering the - value is additionally scattered into :paramref:`backend_buffer` (backend-order staging), and - that staging buffer is the one pushed to the simulation. Otherwise :paramref:`user_buffer` is - pushed directly. - - Args: - target: Joint targets [m, rad, m/s, rad/s, N, or N·m, depending on the setter and joint - type]. Shape is (len(env_ids), len(joint_ids)) for index selection or - (num_instances, num_joints) for mask selection, with dtype wp.float32. - user_buffer: Public-order destination buffer for the target. - backend_buffer: Backend-order staging destination, or None when the joint ordering is - identity. It is guaranteed non-None while a non-identity joint ordering is active - because :meth:`_ordering_configure_backend_staging` allocates it during - initialization. - tensor_type: Backend binding key the target is pushed to. - env_sel: Environment indices (index selection) or mask (mask selection). None selects all. - joint_sel: Joint indices (index selection) or mask (mask selection). None selects all. - use_mask: Whether :paramref:`env_sel` and :paramref:`joint_sel` are masks (True) or - indices (False). - - """ - if use_mask: - env_sel = self._resolve_env_mask(env_sel) - joint_sel = self._resolve_joint_mask(joint_sel) - self.assert_shape_and_dtype(target, (self._num_instances, self._num_joints), wp.float32, "target") - else: - env_sel = self._resolve_env_ids(env_sel) - joint_sel = self._resolve_joint_ids(joint_sel) - self.assert_shape_and_dtype(target, (env_sel.shape[0], joint_sel.shape[0]), wp.float32, "target") - if env_sel.shape[0] == 0 or joint_sel.shape[0] == 0: - return - # Under a non-identity ordering the backend staging receives the reordered copy and is the - # buffer pushed to the binding; the identity case writes and pushes the public buffer. - has_joint_ordering = self.data.has_joint_ordering - if has_joint_ordering: - target_backend = backend_buffer - else: - target_backend = user_buffer - if use_mask: - ordering_kernels.write_float_user_to_backend_with_mask( - target, - env_sel, - joint_sel, - self._joint_user_to_backend_map(), - has_joint_ordering, - user_buffer, - target_backend, - device=self._device, - ) - self._root_view.set_attribute(tensor_type, target_backend, mask=env_sel) - else: - sim_env_ids = self._sim_env_ids_view(env_sel.shape[0]) - ordering_kernels.write_float_user_to_backend_with_indices_and_sim_ids( - target, - env_sel, - joint_sel, - self._joint_user_to_backend_map(), - has_joint_ordering, - False, - user_buffer, - target_backend, - sim_env_ids, - device=self._device, - ) - self._root_view.set_attribute( - tensor_type, target_backend, indices=self._get_sim_env_ids(env_sel, sim_env_ids) - ) - - def set_joint_position_target_index( - self, - *, - target: torch.Tensor | wp.array, - joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - ) -> None: - """Set joint position targets into internal buffers using indices. - - This function does not apply the joint targets to the simulation. It only fills the - buffers with the desired values. To apply the joint targets, call - :meth:`write_data_to_sim`. - - .. note:: - This method expects partial data. - - .. tip:: - Both the index and mask methods have dedicated optimized implementations. - Performance is similar for both. However, to allow graphed pipelines, the - mask method must be used. - - Args: - target: Joint position targets [m or rad, depending on joint type]. Shape is - (len(env_ids), len(joint_ids)) with dtype wp.float32. - joint_ids: Joint indices. Defaults to None (all joints). - env_ids: Environment indices. Defaults to None (all environments). - """ - self._write_joint_target( - target, - user_buffer=self._data._joint_pos_target, - backend_buffer=self._joint_pos_target_backend, - tensor_type=TT.DOF_POSITION_TARGET, - env_sel=env_ids, - joint_sel=joint_ids, - use_mask=False, - ) - - def set_joint_position_target_mask( - self, - *, - target: torch.Tensor | wp.array, - joint_mask: wp.array | None = None, - env_mask: wp.array | None = None, - ) -> None: - """Set joint position targets into internal buffers using masks. - - .. note:: - This method expects full data. - - .. tip:: - Both the index and mask methods have dedicated optimized implementations. - Performance is similar for both. However, to allow graphed pipelines, the - mask method must be used. - - Args: - target: Joint position targets [m or rad, depending on joint type]. Shape is - (num_instances, num_joints) with dtype wp.float32. - joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). - env_mask: Environment mask. If None, all instances are updated. Shape is - (num_instances,). - """ - self._write_joint_target( - target, - user_buffer=self._data._joint_pos_target, - backend_buffer=self._joint_pos_target_backend, - tensor_type=TT.DOF_POSITION_TARGET, - env_sel=env_mask, - joint_sel=joint_mask, - use_mask=True, - ) - - def set_joint_velocity_target_index( - self, - *, - target: torch.Tensor | wp.array, - joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - ) -> None: - """Set joint velocity targets into internal buffers using indices. - - This function does not apply the joint targets to the simulation. It only fills the - buffers with the desired values. To apply the joint targets, call - :meth:`write_data_to_sim`. - - .. note:: - This method expects partial data. - - .. tip:: - Both the index and mask methods have dedicated optimized implementations. - Performance is similar for both. However, to allow graphed pipelines, the - mask method must be used. - - Args: - target: Joint velocity targets [m/s or rad/s, depending on joint type]. Shape is - (len(env_ids), len(joint_ids)) with dtype wp.float32. - joint_ids: Joint indices. Defaults to None (all joints). - env_ids: Environment indices. Defaults to None (all environments). - """ - self._write_joint_target( - target, - user_buffer=self._data._joint_vel_target, - backend_buffer=self._joint_vel_target_backend, - tensor_type=TT.DOF_VELOCITY_TARGET, - env_sel=env_ids, - joint_sel=joint_ids, - use_mask=False, - ) - - def set_joint_velocity_target_mask( - self, - *, - target: torch.Tensor | wp.array, - joint_mask: wp.array | None = None, - env_mask: wp.array | None = None, - ) -> None: - """Set joint velocity targets into internal buffers using masks. - - .. note:: - This method expects full data. - - .. tip:: - Both the index and mask methods have dedicated optimized implementations. - Performance is similar for both. However, to allow graphed pipelines, the - mask method must be used. - - Args: - target: Joint velocity targets [m/s or rad/s, depending on joint type]. Shape is - (num_instances, num_joints) with dtype wp.float32. - joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). - env_mask: Environment mask. If None, all instances are updated. Shape is - (num_instances,). - """ - self._write_joint_target( - target, - user_buffer=self._data._joint_vel_target, - backend_buffer=self._joint_vel_target_backend, - tensor_type=TT.DOF_VELOCITY_TARGET, - env_sel=env_mask, - joint_sel=joint_mask, - use_mask=True, - ) - - def set_joint_effort_target_index( - self, - *, - target: torch.Tensor | wp.array, - joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - ) -> None: - """Set joint effort targets into internal buffers using indices. - - This function does not apply the joint targets to the simulation. It only fills the - buffers with the desired values. To apply the joint targets, call - :meth:`write_data_to_sim`. - - .. note:: - This method expects partial data. - - .. tip:: - Both the index and mask methods have dedicated optimized implementations. - Performance is similar for both. However, to allow graphed pipelines, the - mask method must be used. - - Args: - target: Joint effort targets [N or N·m, depending on joint type]. Shape is - (len(env_ids), len(joint_ids)) with dtype wp.float32. - joint_ids: Joint indices. Defaults to None (all joints). - env_ids: Environment indices. Defaults to None (all environments). - """ - self._write_joint_target( - target, - user_buffer=self._data._joint_effort_target, - backend_buffer=self._joint_effort_target_backend, - tensor_type=TT.DOF_ACTUATION_FORCE, - env_sel=env_ids, - joint_sel=joint_ids, - use_mask=False, - ) - - def set_joint_effort_target_mask( - self, - *, - target: torch.Tensor | wp.array, - joint_mask: wp.array | None = None, - env_mask: wp.array | None = None, - ) -> None: - """Set joint effort targets into internal buffers using masks. - - .. note:: - This method expects full data. - - .. tip:: - Both the index and mask methods have dedicated optimized implementations. - Performance is similar for both. However, to allow graphed pipelines, the - mask method must be used. - - Args: - target: Joint effort targets [N or N·m, depending on joint type]. Shape is - (num_instances, num_joints) with dtype wp.float32. - joint_mask: Joint mask. If None, all joints are updated. Shape is (num_joints,). - env_mask: Environment mask. If None, all instances are updated. Shape is - (num_instances,). - """ - self._write_joint_target( - target, - user_buffer=self._data._joint_effort_target, - backend_buffer=self._joint_effort_target_backend, - tensor_type=TT.DOF_ACTUATION_FORCE, - env_sel=env_mask, - joint_sel=joint_mask, - use_mask=True, - ) - """ Operations - Tendons. """ @@ -4275,7 +3936,7 @@ def _create_buffers(self) -> None: self._joint_pos_target_backend: wp.array | None = None self._joint_vel_target_backend: wp.array | None = None self._joint_effort_target_backend: wp.array | None = None - self._applied_torque_backend: wp.array | None = None + self._applied_effort_backend: wp.array | None = None self._ordering_configure_backend_staging() # All-true masks. @@ -4294,10 +3955,6 @@ def _create_buffers(self) -> None: self._instantaneous_wrench_composer = WrenchComposer(self) self._permanent_wrench_composer = WrenchComposer(self) - # Wrench scratch buffer (used by _apply_external_wrenches, not yet allocated above). - # Joint-index arrays for each actuator (populated by _process_actuators_cfg). - self._joint_ids_per_actuator: dict[str, slice | torch.Tensor] = {} - # Pinned-host CPU staging for env ids/masks (PR #5329 pattern). self._cpu_env_ids_all = wp.zeros(N, dtype=wp.int32, device="cpu", pinned=True) wp.copy(self._cpu_env_ids_all, self._ALL_INDICES) @@ -4471,17 +4128,18 @@ def _invalidate_initialize_callback(self, event) -> None: "_joint_pos_target_backend", "_joint_vel_target_backend", "_joint_effort_target_backend", - "_applied_torque_backend", + "_applied_effort_backend", ) """Backend-order joint staging buffers managed by :meth:`_ordering_configure_backend_staging`. ``_joint_pos_target_backend`` / ``_joint_vel_target_backend`` / ``_joint_effort_target_backend`` are persistent backend-order mirrors of the corresponding user-order target buffers, kept - current by the partial :meth:`set_joint_position_target_index`-style setters. - ``_applied_torque_backend`` is separate, purely transient scratch: :meth:`write_data_to_sim` - fully overwrites it every step with the backend-order actuator output, so it must not alias - ``_joint_effort_target_backend`` (whose unselected rows a partial effort-target write relies - on to still hold the persisted target, not the last pushed applied torque). + current by :meth:`OvPhysxActuatorControl.stage_user_command` whenever a user setter runs. + ``_applied_effort_backend`` is separate, purely transient scratch: + :meth:`OvPhysxActuatorControl.submit_commands` fully overwrites it every step with the + backend-order actuator output, so it must not alias ``_joint_effort_target_backend`` (whose + unselected rows a partial effort-target write relies on to still hold the persisted target, + not the last pushed applied torque). """ """ @@ -4489,115 +4147,17 @@ def _invalidate_initialize_callback(self, event) -> None: """ def _process_actuators_cfg(self) -> None: - """Build actuator instances from the config and write drive properties to PhysX. - - Mirrors the PhysX backend's ``_process_actuators_cfg``: - - * For :class:`~isaaclab.actuators.ImplicitActuator`: write the configured - stiffness/damping to the PhysX drive so the solver uses exactly those values. - * For all explicit actuators: zero out PhysX stiffness/damping so USD-authored - drive gains cannot interfere with the explicit torque path. - * For all actuators: write :attr:`~isaaclab.actuators.ActuatorBase.effort_limit_sim` - and :attr:`~isaaclab.actuators.ActuatorBase.velocity_limit_sim`. - """ - from isaaclab.actuators import ImplicitActuator - - self.actuators: dict[str, Any] = {} - self._has_implicit_actuators = False - for name, act_cfg in self.cfg.actuators.items(): - joint_ids, joint_names = self.find_joints(act_cfg.joint_names_expr, as_proxy=True) - if not joint_names: - logger.warning("Actuator '%s': no joints matched '%s'", name, act_cfg.joint_names_expr) - continue - actuator_joint_ids = slice(None) if joint_names == self.joint_names else joint_ids.torch - torch_joint_ids = actuator_joint_ids - act_cfg_copy = act_cfg.copy() - # seed the actuator with the simulation's already-correct DOF defaults - # (USD-authored ``physxJoint:maxJointVelocity`` etc. parsed at scene-load). - # Without these the ActuatorBase constructor falls back to ``inf`` for unset - # cfg fields, and the ``write_joint_*_to_sim_index`` calls below then - # overwrite the correct values with ``inf``. - act = act_cfg_copy.class_type( - act_cfg_copy, - joint_names=joint_names, - joint_ids=actuator_joint_ids, - num_envs=self._num_instances, - device=self._device, - stiffness=self._data.joint_stiffness.torch[:, torch_joint_ids], - damping=self._data.joint_damping.torch[:, torch_joint_ids], - armature=self._data.joint_armature.torch[:, torch_joint_ids], - friction=self._data.joint_friction_coeff.torch[:, torch_joint_ids], - dynamic_friction=self._data.joint_dynamic_friction_coeff.torch[:, torch_joint_ids], - viscous_friction=self._data.joint_viscous_friction_coeff.torch[:, torch_joint_ids], - effort_limit=self._data.joint_effort_limits.torch[:, torch_joint_ids].clone(), - velocity_limit=self._data.joint_vel_limits.torch[:, torch_joint_ids], - ) - self.actuators[name] = act - self._joint_ids_per_actuator[name] = actuator_joint_ids - - # Write drive gains and limits to PhysX to match the actuator config. - # Without this, PhysX retains whatever stiffness/damping was authored in the - # USD file, which can produce large restoring forces when the USD gains differ - # from the actuator config. - if isinstance(act, ImplicitActuator): - self._has_implicit_actuators = True - stiffness = act.stiffness # torch (N, J) - damping = act.damping # torch (N, J) - else: - stiffness = wp.zeros((self._num_instances, len(joint_names)), dtype=wp.float32, device=self._device) - damping = wp.zeros((self._num_instances, len(joint_names)), dtype=wp.float32, device=self._device) - self.write_joint_stiffness_to_sim_index(stiffness=stiffness, joint_ids=actuator_joint_ids) - self.write_joint_damping_to_sim_index(damping=damping, joint_ids=actuator_joint_ids) - self.write_joint_effort_limit_to_sim_index(limits=act.effort_limit_sim, joint_ids=actuator_joint_ids) - self.write_joint_velocity_limit_to_sim_index(limits=act.velocity_limit_sim, joint_ids=actuator_joint_ids) - - def _apply_actuator_model(self) -> None: - """Run the actuator model to compute joint torques from user-supplied targets. - - IsaacLab actuators are torch-based. The method converts Warp buffers to - torch via DLPack (zero-copy on GPU), runs each actuator's - :meth:`~isaaclab.actuators.ActuatorBase.compute` method, then writes the - computed effort back to the private ``_computed_torque`` / ``_applied_torque`` - buffers of the data container. :meth:`write_data_to_sim` then pushes - ``_applied_torque`` to the ``DOF_ACTUATION_FORCE`` binding in one shot. - """ - from isaaclab.utils.types import ArticulationActions - - for name, act in self.actuators.items(): - joint_ids = self._joint_ids_per_actuator[name] - all_joints = isinstance(joint_ids, slice) - torch_joint_ids = joint_ids - - # Warp -> torch (zero-copy on same device via DLPack). - jp_target_full = self._data.joint_pos_target.torch - jv_target_full = self._data.joint_vel_target.torch - je_target_full = self._data.joint_effort_target.torch - jp_target = jp_target_full if all_joints else jp_target_full[:, torch_joint_ids] - jv_target = jv_target_full if all_joints else jv_target_full[:, torch_joint_ids] - je_target = je_target_full if all_joints else je_target_full[:, torch_joint_ids] - - control_action = ArticulationActions( - joint_positions=jp_target, - joint_velocities=jv_target, - joint_efforts=je_target, - ) - - jp_cur_full = self._data.joint_pos.torch - jv_cur_full = self._data.joint_vel.torch - jp_cur = jp_cur_full if all_joints else jp_cur_full[:, torch_joint_ids] - jv_cur = jv_cur_full if all_joints else jv_cur_full[:, torch_joint_ids] - - control_action = act.compute(control_action, jp_cur, jv_cur) - - if act.computed_effort is not None: - ct = wp.to_torch(self._data._computed_torque) - at = wp.to_torch(self._data._applied_torque) - if all_joints: - ct[:] = act.computed_effort - at[:] = act.applied_effort - else: - ct[:, torch_joint_ids] = act.computed_effort - at[:, torch_joint_ids] = act.applied_effort + """Build actuator instances and delegate runtime ownership to the collection.""" + self._actuator_control = OvPhysxActuatorControl(self) + self.actuators = ActuatorCollection( + self.cfg.actuators, + self._actuator_control, + debug_value_resolution=self.cfg.actuator_value_resolution_debug_print, + ) + self._has_implicit_actuators = self.actuators.has_implicit_actuators + self._has_newton_actuators = self._actuator_control.native_actuator_path_active + self._physx_actuator_wrapper = self._actuator_control._physx_actuator_wrapper + self._data.bind_actuator_collection(self.actuators) """ Internal helpers -- Debugging. diff --git a/source/isaaclab_ov/isaaclab_ov/assets/articulation/articulation_data.py b/source/isaaclab_ov/isaaclab_ov/assets/articulation/articulation_data.py index 790be94fe4a8..3301219e6db2 100644 --- a/source/isaaclab_ov/isaaclab_ov/assets/articulation/articulation_data.py +++ b/source/isaaclab_ov/isaaclab_ov/assets/articulation/articulation_data.py @@ -440,64 +440,6 @@ def default_joint_vel(self, value: wp.array) -> None: raise ValueError("The articulation data is already primed.") self._default_joint_vel.assign(value) - """ - Joint commands -- Set into simulation. - """ - - @property - def joint_pos_target(self) -> ProxyArray: - """Joint position targets commanded by the user [m or rad, depending on joint type]. - - Shape is (num_instances, num_joints), dtype = wp.float32. - """ - if self._joint_pos_target_ta is None: - self._joint_pos_target_ta = ProxyArray(self._joint_pos_target) - return self._joint_pos_target_ta - - @property - def joint_vel_target(self) -> ProxyArray: - """Joint velocity targets commanded by the user [m/s or rad/s, depending on joint type]. - - Shape is (num_instances, num_joints), dtype = wp.float32. - """ - if self._joint_vel_target_ta is None: - self._joint_vel_target_ta = ProxyArray(self._joint_vel_target) - return self._joint_vel_target_ta - - @property - def joint_effort_target(self) -> ProxyArray: - """Joint effort targets commanded by the user [N or N*m, depending on joint type]. - - Shape is (num_instances, num_joints), dtype = wp.float32. - """ - if self._joint_effort_target_ta is None: - self._joint_effort_target_ta = ProxyArray(self._joint_effort_target) - return self._joint_effort_target_ta - - """ - Joint commands -- Explicit actuators. - """ - - @property - def computed_torque(self) -> ProxyArray: - """Joint torques computed from the actuator model (before clipping) [N*m]. - - Shape is (num_instances, num_joints), dtype = wp.float32. - """ - if self._computed_torque_ta is None: - self._computed_torque_ta = ProxyArray(self._computed_torque) - return self._computed_torque_ta - - @property - def applied_torque(self) -> ProxyArray: - """Joint torques applied from the actuator model (after clipping) [N*m]. - - Shape is (num_instances, num_joints), dtype = wp.float32. - """ - if self._applied_torque_ta is None: - self._applied_torque_ta = ProxyArray(self._applied_torque) - return self._applied_torque_ta - """ Joint properties """ @@ -651,26 +593,6 @@ def soft_joint_pos_limits(self) -> ProxyArray: self._soft_joint_pos_limits_ta = ProxyArray(self._soft_joint_pos_limits) return self._soft_joint_pos_limits_ta - @property - def soft_joint_vel_limits(self) -> ProxyArray: - """Soft joint velocity limits for all joints [m/s or rad/s, depending on joint type]. - - Shape is (num_instances, num_joints), dtype = wp.float32. - """ - if self._soft_joint_vel_limits_ta is None: - self._soft_joint_vel_limits_ta = ProxyArray(self._soft_joint_vel_limits) - return self._soft_joint_vel_limits_ta - - @property - def gear_ratio(self) -> ProxyArray: - """Gear ratio for relating motor torques to applied joint torques. - - Shape is (num_instances, num_joints), dtype = wp.float32. - """ - if self._gear_ratio_ta is None: - self._gear_ratio_ta = ProxyArray(self._gear_ratio) - return self._gear_ratio_ta - """ Fixed tendon properties. """ @@ -1818,12 +1740,8 @@ def _create_buffers(self) -> None: # noqa: C901 # -- Soft limits / custom joint properties self._soft_joint_pos_limits = wp.zeros((N, D), dtype=wp.vec2f, device=dev) self._soft_joint_vel_limits = wp.zeros((N, D), dtype=wp.float32, device=dev) - self._gear_ratio = wp.ones((N, D), dtype=wp.float32, device=dev) - # -- Command buffers - self._joint_pos_target = wp.zeros((N, D), dtype=wp.float32, device=dev) - self._joint_vel_target = wp.zeros((N, D), dtype=wp.float32, device=dev) - self._joint_effort_target = wp.zeros((N, D), dtype=wp.float32, device=dev) + # -- Actuator telemetry buffers self._computed_torque = wp.zeros((N, D), dtype=wp.float32, device=dev) self._applied_torque = wp.zeros((N, D), dtype=wp.float32, device=dev) @@ -2276,7 +2194,6 @@ def _pin_proxy_arrays(self) -> None: # Joint properties (custom) self._soft_joint_pos_limits_ta: ProxyArray | None = None self._soft_joint_vel_limits_ta: ProxyArray | None = None - self._gear_ratio_ta: ProxyArray | None = None # Fixed tendon properties self._fixed_tendon_stiffness_ta: ProxyArray | None = None self._fixed_tendon_damping_ta: ProxyArray | None = None diff --git a/source/isaaclab_ov/isaaclab_ov/benchmark/assets/runtime.py b/source/isaaclab_ov/isaaclab_ov/benchmark/assets/runtime.py index 2c76abbdd35c..c13a91115233 100644 --- a/source/isaaclab_ov/isaaclab_ov/benchmark/assets/runtime.py +++ b/source/isaaclab_ov/isaaclab_ov/benchmark/assets/runtime.py @@ -74,10 +74,17 @@ def create_test_articulation( data = ArticulationData(mock_view, device) data._apply_ordering_maps_after_resolve() object.__setattr__(articulation, "_data", data) - object.__setattr__(articulation, "actuators", {}) object.__setattr__(articulation, "_has_implicit_actuators", False) articulation._create_buffers() + from isaaclab.actuators import ActuatorCollection + + from isaaclab_ov.assets.articulation.actuator_control import OvPhysxActuatorControl + + control = OvPhysxActuatorControl(articulation) + object.__setattr__(articulation, "actuators", ActuatorCollection({}, control)) + data.bind_actuator_collection(articulation.actuators) + return articulation, mock_view diff --git a/source/isaaclab_ov/test/assets/test_articulation.py b/source/isaaclab_ov/test/assets/test_articulation.py index 97e15fdfe696..706f623ca11a 100644 --- a/source/isaaclab_ov/test/assets/test_articulation.py +++ b/source/isaaclab_ov/test/assets/test_articulation.py @@ -50,6 +50,7 @@ from __future__ import annotations +import importlib import sys from pathlib import Path from unittest.mock import Mock @@ -82,7 +83,7 @@ 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 ActuatorBase, IdealPDActuatorCfg, ImplicitActuatorCfg # 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 @@ -95,7 +96,7 @@ ## # Pre-defined configs ## -from isaaclab_assets import ANYMAL_C_CFG, FRANKA_PANDA_CFG, SHADOW_HAND_CFG # isort:skip +from isaaclab_assets import ANYMAL_C_CFG, CARTPOLE_CFG, FRANKA_PANDA_CFG, SHADOW_HAND_CFG # isort:skip wp.init() @@ -249,8 +250,15 @@ def _ovphysx_sim_context(device: str, **kwargs): """ 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) + 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) @@ -258,10 +266,10 @@ def generate_articulation_cfg( articulation_type: str, stiffness: float | None = 10.0, damping: float | None = 2.0, - velocity_limit: float | None = None, - effort_limit: float | None = None, - velocity_limit_sim: float | None = None, - effort_limit_sim: float | None = None, + 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. @@ -273,13 +281,13 @@ def generate_articulation_cfg( Defaults to 10.0. damping: Damping value for the articulation's actuators. Only currently used for "humanoid". Defaults to 2.0. - velocity_limit: Velocity limit for the actuators. Only currently used for "single_joint_implicit" + actuator_velocity_limit: Velocity limit for the actuators. Only currently used for "single_joint_implicit" and "single_joint_explicit". - effort_limit: Effort limit for the actuators. Only currently used for "single_joint_implicit" - and "single_joint_explicit". - velocity_limit_sim: Velocity limit for the actuators (set into the simulation). + 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". - effort_limit_sim: Effort limit for the actuators (set into the simulation). + joint_effort_limit: Effort limit for the actuators (set into the simulation). Only currently used for "single_joint_implicit" and "single_joint_explicit". Returns: @@ -310,10 +318,9 @@ def generate_articulation_cfg( actuators={ "joint": ImplicitActuatorCfg( joint_names_expr=[".*"], - effort_limit_sim=effort_limit_sim, - velocity_limit_sim=velocity_limit_sim, - effort_limit=effort_limit, - velocity_limit=velocity_limit, + joint_effort_limit=joint_effort_limit, + joint_velocity_limit=joint_velocity_limit, + actuator_velocity_limit=actuator_velocity_limit, stiffness=2000.0, damping=100.0, ), @@ -334,10 +341,10 @@ def generate_articulation_cfg( actuators={ "joint": IdealPDActuatorCfg( joint_names_expr=[".*"], - effort_limit_sim=effort_limit_sim, - velocity_limit_sim=velocity_limit_sim, - effort_limit=effort_limit, - velocity_limit=velocity_limit, + 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, ), @@ -395,6 +402,178 @@ def generate_articulation( 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.""" @@ -2296,36 +2475,24 @@ def test_setting_gains_from_cfg_dict(sim, num_articulations, device): torch.testing.assert_close(articulation.actuators["body"].damping, expected_damping) -@pytest.mark.parametrize("num_articulations", [1, 2]) @pytest.mark.parametrize("device", test_devices()) -@pytest.mark.parametrize("vel_limit_sim", [1e5, None]) -@pytest.mark.parametrize("vel_limit", [1e2, None]) -@pytest.mark.parametrize("add_ground_plane", [False]) -def test_setting_velocity_limit_implicit(sim, num_articulations, device, vel_limit_sim, vel_limit, add_ground_plane): - """Test setting of velocity limit for implicit actuators. - - This test verifies that: - 1. The solver clamp ``velocity_limit_sim`` is applied to the simulation; when unset, the - USD-authored value is kept - 2. The joint velocity limit ``velocity_limit`` is never pushed to the solver and keeps its - configured value; when unset, it falls back to the solver clamp - - Args: - sim: The simulation fixture - num_articulations: Number of articulations to test - device: The device to run the simulation on - vel_limit_sim: The velocity limit to set in simulation - vel_limit: The velocity limit to set in actuator +@pytest.mark.parametrize("joint_velocity_limit", [1e5, None]) +def test_setting_velocity_limit_writes_to_solver(sim, device, joint_velocity_limit): + """Test that the resolved joint velocity limit reaches the PhysX tensor-API solver. + + The full limit-resolution matrix (config override vs. USD default, implicit and explicit + actuators, actuator-limit soft fallback) is covered on the Newton backend and at unit + level. This smoke test only verifies the PhysX tensor-API write path: the configured limit (or the + USD-authored default when unset) lands in the native solver buffers and matches + ``data.joint_vel_limits``. """ - # create simulation articulation_cfg = generate_articulation_cfg( articulation_type="single_joint_implicit", - velocity_limit_sim=vel_limit_sim, - velocity_limit=vel_limit, + joint_velocity_limit=joint_velocity_limit, ) articulation, _ = generate_articulation( articulation_cfg=articulation_cfg, - num_articulations=num_articulations, + num_articulations=1, device=device, ) # Play sim @@ -2334,98 +2501,34 @@ def test_setting_velocity_limit_implicit(sim, num_articulations, device, vel_lim # read the values set into the simulation physx_vel_limit = _read_binding_to_torch(articulation, TT.DOF_MAX_VELOCITY, device) # check data buffer - torch.testing.assert_close(articulation.data.joint_velocity_limits.torch, physx_vel_limit) - # check actuator has simulation velocity limit - torch.testing.assert_close(articulation.actuators["joint"].velocity_limit_sim, physx_vel_limit) - - # the solver clamp comes from velocity_limit_sim when set, otherwise the USD-authored value - if vel_limit_sim is None: - sim_limit = articulation_cfg.spawn.joint_drive_props.max_joint_velocity + torch.testing.assert_close(articulation.data.joint_vel_limits.torch, physx_vel_limit) + # the solver clamp comes from joint_velocity_limit when set, otherwise the USD-authored value + if joint_velocity_limit is None: + limit = articulation_cfg.spawn.joint_drive_props.max_joint_velocity else: - sim_limit = vel_limit_sim - expected_velocity_limit = torch.full_like(physx_vel_limit, sim_limit) + limit = joint_velocity_limit + expected_velocity_limit = torch.full_like(physx_vel_limit, limit) torch.testing.assert_close(physx_vel_limit, expected_velocity_limit) - # the joint velocity limit keeps its configured value and is not pushed to the solver; - # when unset it falls back to the solver clamp - joint_limit = vel_limit if vel_limit is not None else sim_limit - expected_joint_limit = torch.full_like(physx_vel_limit, joint_limit) - torch.testing.assert_close(articulation.actuators["joint"].velocity_limit, expected_joint_limit) - -@pytest.mark.parametrize("num_articulations", [1, 2]) @pytest.mark.parametrize("device", test_devices()) -@pytest.mark.parametrize("vel_limit_sim", [1e5, None]) -@pytest.mark.parametrize("vel_limit", [1e2, None]) -def test_setting_velocity_limit_explicit(sim, num_articulations, device, vel_limit_sim, vel_limit): - """Test setting of velocity limit for explicit actuators.""" - articulation_cfg = generate_articulation_cfg( - articulation_type="single_joint_explicit", - velocity_limit_sim=vel_limit_sim, - velocity_limit=vel_limit, - ) - articulation, _ = generate_articulation( - articulation_cfg=articulation_cfg, - num_articulations=num_articulations, - device=device, - ) - # Play sim - sim.reset() - - # collect limit init values - physx_vel_limit = _read_binding_to_torch(articulation, TT.DOF_MAX_VELOCITY, device) - actuator_vel_limit = articulation.actuators["joint"].velocity_limit - actuator_vel_limit_sim = articulation.actuators["joint"].velocity_limit_sim - - # check data buffer for joint_velocity_limits_sim - torch.testing.assert_close(articulation.data.joint_velocity_limits.torch, physx_vel_limit) - # check actuator velocity_limit_sim is set to physx - torch.testing.assert_close(actuator_vel_limit_sim, physx_vel_limit) - - if vel_limit is not None: - expected_actuator_vel_limit = torch.full( - (articulation.num_instances, articulation.num_joints), - vel_limit, - device=articulation.device, - ) - # check actuator is set - torch.testing.assert_close(actuator_vel_limit, expected_actuator_vel_limit) - # check physx is not velocity_limit - assert not torch.allclose(actuator_vel_limit, physx_vel_limit) - else: - # check actuator velocity_limit is the same as the PhysX default - torch.testing.assert_close(actuator_vel_limit, physx_vel_limit) - - # simulation velocity limit is set to USD value unless user overrides - if vel_limit_sim is not None: - limit = vel_limit_sim - else: - limit = articulation_cfg.spawn.joint_drive_props.max_joint_velocity - # check physx is set to expected value - expected_vel_limit = torch.full_like(physx_vel_limit, limit) - torch.testing.assert_close(physx_vel_limit, expected_vel_limit) - - -@pytest.mark.parametrize("num_articulations", [1, 2]) -@pytest.mark.parametrize("device", test_devices()) -@pytest.mark.parametrize("effort_limit_sim", [1e5, None]) -@pytest.mark.parametrize("effort_limit", [1e2, 80.0, None]) -def test_setting_effort_limit_implicit(sim, num_articulations, device, effort_limit_sim, effort_limit): - """Test setting of effort limit for implicit actuators. - - This test verifies the effort limit resolution logic for actuator models implemented in :class:`ActuatorBase`: - - Case 1: If USD value == actuator config value: values match correctly - - Case 2: If USD value != actuator config value: actuator config value is used - - Case 3: If actuator config value is None: USD value is used as default +@pytest.mark.parametrize("joint_effort_limit", [1e5, None]) +def test_setting_effort_limit_writes_to_solver(sim, device, joint_effort_limit): + """Test that the resolved joint effort limit reaches the PhysX tensor-API solver. + + The full limit-resolution matrix (config override vs. USD default, implicit and explicit + actuators, actuator-limit soft fallback) is covered on the Newton backend and at unit + level. This smoke test only verifies the PhysX tensor-API write path: the configured limit (or the + USD-authored default when unset) lands in the native solver buffers and matches + ``data.joint_effort_limits``. """ articulation_cfg = generate_articulation_cfg( articulation_type="single_joint_implicit", - effort_limit_sim=effort_limit_sim, - effort_limit=effort_limit, + joint_effort_limit=joint_effort_limit, ) articulation, _ = generate_articulation( articulation_cfg=articulation_cfg, - num_articulations=num_articulations, + num_articulations=1, device=device, ) # Play sim @@ -2433,78 +2536,14 @@ def test_setting_effort_limit_implicit(sim, num_articulations, device, effort_li # obtain the physx effort limits physx_effort_limit = _read_binding_to_torch(articulation, TT.DOF_MAX_FORCE, device) - - # The solver clamp reaches the physics engine; the rated limit remains on the actuator. - torch.testing.assert_close(articulation.actuators["joint"].effort_limit_sim, physx_effort_limit) - - solver_limit = effort_limit_sim if effort_limit_sim is not None else effort_limit - if solver_limit is None: - solver_limit = articulation_cfg.spawn.joint_drive_props.max_force - rated_limit = effort_limit if effort_limit is not None else solver_limit - torch.testing.assert_close(physx_effort_limit, torch.full_like(physx_effort_limit, solver_limit)) - torch.testing.assert_close( - articulation.actuators["joint"].effort_limit, torch.full_like(physx_effort_limit, rated_limit) - ) - - -@pytest.mark.parametrize("num_articulations", [1, 2]) -@pytest.mark.parametrize("device", test_devices()) -@pytest.mark.parametrize("effort_limit_sim", [1e5, None]) -@pytest.mark.parametrize("effort_limit", [80.0, 1e2, None]) -def test_setting_effort_limit_explicit(sim, num_articulations, device, effort_limit_sim, effort_limit): - """Test setting of effort limit for explicit actuators. - - This test verifies the effort limit resolution logic for actuator models implemented in :class:`ActuatorBase`: - - Case 1: If USD value == actuator config value: values match correctly - - Case 2: If USD value != actuator config value: actuator config value is used - - Case 3: If actuator config value is None: USD value is used as default - - """ - - articulation_cfg = generate_articulation_cfg( - articulation_type="single_joint_explicit", - effort_limit_sim=effort_limit_sim, - effort_limit=effort_limit, - ) - articulation, _ = generate_articulation( - articulation_cfg=articulation_cfg, - num_articulations=num_articulations, - device=device, - ) - # Play sim - sim.reset() - - # usd default effort limit is set to 80 - usd_default_effort_limit = 80.0 - - # collect limit init values - physx_effort_limit = _read_binding_to_torch(articulation, TT.DOF_MAX_FORCE, device) - actuator_effort_limit = articulation.actuators["joint"].effort_limit - actuator_effort_limit_sim = articulation.actuators["joint"].effort_limit_sim - - # check actuator effort_limit_sim is set to physx - torch.testing.assert_close(actuator_effort_limit_sim, physx_effort_limit) - - if effort_limit is not None: - expected_actuator_effort_limit = torch.full_like(actuator_effort_limit, effort_limit) - # check actuator is set - torch.testing.assert_close(actuator_effort_limit, expected_actuator_effort_limit) - - # check physx effort limit does not match the one explicit actuator has - assert not (torch.allclose(actuator_effort_limit, physx_effort_limit)) - else: - # When effort_limit is None, actuator should use USD default values - expected_actuator_effort_limit = torch.full_like(physx_effort_limit, usd_default_effort_limit) - torch.testing.assert_close(actuator_effort_limit, expected_actuator_effort_limit) - - # when using explicit actuators, the limits are set to high unless user overrides - if effort_limit_sim is not None: - limit = effort_limit_sim + # check data buffer + torch.testing.assert_close(articulation.data.joint_effort_limits.torch, physx_effort_limit) + # the solver keeps the USD-authored limit unless the user overrides it explicitly + if joint_effort_limit is None: + limit = articulation_cfg.spawn.joint_drive_props.max_force else: - limit = ActuatorBase._DEFAULT_MAX_EFFORT_SIM # type: ignore - # check physx internal value matches the expected sim value + limit = joint_effort_limit expected_effort_limit = torch.full_like(physx_effort_limit, limit) - torch.testing.assert_close(actuator_effort_limit_sim, expected_effort_limit) torch.testing.assert_close(physx_effort_limit, expected_effort_limit) diff --git a/source/isaaclab_ov/test/sensors/test_joint_wrench_sensor.py b/source/isaaclab_ov/test/sensors/test_joint_wrench_sensor.py index 5a33b9ad97a6..8eee9ff4d96e 100644 --- a/source/isaaclab_ov/test/sensors/test_joint_wrench_sensor.py +++ b/source/isaaclab_ov/test/sensors/test_joint_wrench_sensor.py @@ -134,10 +134,10 @@ def _make_cartpole_articulation_cfg(pole_damping: float = 0.0) -> ArticulationCf ), actuators={ "cart_actuator": ImplicitActuatorCfg( - joint_names_expr=["slider_to_cart"], effort_limit_sim=400.0, stiffness=0.0, damping=10.0 + joint_names_expr=["slider_to_cart"], joint_effort_limit=400.0, stiffness=0.0, damping=10.0 ), "pole_actuator": ImplicitActuatorCfg( - joint_names_expr=["cart_to_pole"], effort_limit_sim=400.0, stiffness=0.0, damping=pole_damping + joint_names_expr=["cart_to_pole"], joint_effort_limit=400.0, stiffness=0.0, damping=pole_damping ), }, ) diff --git a/source/isaaclab_physx/changelog.d/actuator-collection.minor.rst b/source/isaaclab_physx/changelog.d/actuator-collection.minor.rst new file mode 100644 index 000000000000..aa9a98ba0470 --- /dev/null +++ b/source/isaaclab_physx/changelog.d/actuator-collection.minor.rst @@ -0,0 +1,13 @@ +Added +^^^^^ + +* Added CUDA graph replay for graphable Newton actuators running on the PhysX + backend. + +Changed +^^^^^^^ + +* Routed PhysX articulation actuator setup, compute, reset, and command + submission through :class:`~isaaclab.actuators.ActuatorCollection`. +* Prevented stateful Newton actuators from running inside caller-owned CUDA + graph captures; let the PhysX adapter manage their alternating graphs. diff --git a/source/isaaclab_physx/isaaclab_physx/assets/articulation/actuator_control.py b/source/isaaclab_physx/isaaclab_physx/assets/articulation/actuator_control.py new file mode 100644 index 000000000000..f8cbc6b49bf4 --- /dev/null +++ b/source/isaaclab_physx/isaaclab_physx/assets/articulation/actuator_control.py @@ -0,0 +1,160 @@ +# 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 + +"""PhysX actuator control adapter.""" + +from __future__ import annotations + +import logging +from collections.abc import Sequence +from typing import TYPE_CHECKING + +from isaaclab.actuators import ActuatorCollection +from isaaclab.actuators.actuator_base_cfg import _is_implicit_actuator_cfg +from isaaclab.actuators.actuator_control import ArticulationActuatorControl +from isaaclab.actuators.newton.adapter import LightArticulationView, NewtonActuatorSelection +from isaaclab.actuators.newton.physx_runtime import PhysxActuatorRuntime +from isaaclab.assets.articulation import ordering_kernels +from isaaclab.sim.schemas.schemas_actuators import _validate_newton_native_actuator_cfgs +from isaaclab.sim.utils.queries import find_first_matching_prim +from isaaclab.sim.utils.stage import get_current_stage + +if TYPE_CHECKING: + from .articulation import Articulation + +logger = logging.getLogger(__name__) + + +class PhysxActuatorControl(ArticulationActuatorControl): + """Actuator control adapter for the PhysX backend.""" + + def __init__(self, articulation: Articulation): + """Initialize the control adapter. + + Args: + articulation: PhysX articulation that owns backend simulation handles. + """ + super().__init__(articulation) + self._actuator_runtime = None + + @property + def _physx_actuator_wrapper(self): + """Preserve the compatibility wrapper used by native actuator callers.""" + return None if self._actuator_runtime is None else self._actuator_runtime.wrapper + + @property + def _native_actuator_graphs(self): + """Expose graph capture state used by native-actuator benchmarks.""" + return None if self._actuator_runtime is None else self._actuator_runtime.native_actuator_graphs + + def prepare_native_actuators(self, collection: ActuatorCollection, actuator_cfgs: dict) -> set[str]: + articulation = self._articulation + self._actuator_runtime = None + articulation._physx_actuator_wrapper = None + articulation.newton_actuator_adapter = None + articulation._implicit_dof_mask = None + articulation._has_newton_actuators = False + + use_newton_actuators = getattr(articulation._sim_cfg, "use_newton_actuators", False) + if not use_newton_actuators: + return set() + + _validate_newton_native_actuator_cfgs(actuator_cfgs) + + # Activate the runtime even without explicit native groups: implicit-only + # articulations still rely on it for the solver telemetry fast path. + self._native_actuator_path_active = True + articulation._has_newton_actuators = True + native_group_names = { + name for name, actuator_cfg in actuator_cfgs.items() if not _is_implicit_actuator_cfg(actuator_cfg) + } + + first_prim = find_first_matching_prim(articulation.cfg.prim_path) + art_prim_path = str(first_prim.GetPath()) if first_prim is not None else None + self._actuator_runtime = PhysxActuatorRuntime(articulation, logger=logger) + self._actuator_runtime.prepare( + collection, + stage=get_current_stage(), + articulation_prim_path=art_prim_path, + adapt_usd_actuators=bool(native_group_names), + ) + articulation._physx_actuator_wrapper = self._actuator_runtime.wrapper + articulation.newton_actuator_adapter = self._actuator_runtime.adapter + + return native_group_names + + def finalize_native_actuators(self, collection: ActuatorCollection) -> NewtonActuatorSelection | None: + if not self._native_actuator_path_active or self._actuator_runtime is None: + return None + self._actuator_runtime.finalize(collection) + adapter = self._actuator_runtime.adapter + if adapter is None: + return None + articulation = self._articulation + return NewtonActuatorSelection( + view=LightArticulationView(articulation.num_instances, articulation.num_joints, articulation.device), + actuators=adapter.actuators, + ) + + def compute_native_actuators(self, collection: ActuatorCollection, dt: float) -> bool: + if not self._native_actuator_path_active or self._actuator_runtime is None: + return False + + articulation = self._articulation + if articulation.data.has_joint_ordering: + # Non-identity ordering binds the wrapper to public shadow buffers. Refresh before + # stepping so the native controller observes this PhysX step's state. + articulation._data._refresh_joint_pos() + articulation._data._refresh_joint_vel() + self._actuator_runtime.compute(collection, dt) + return True + + def submit_commands(self, collection: ActuatorCollection) -> None: + articulation = self._articulation + # The articulation flag selects the native wrapper's command buffers. + if getattr(articulation, "_has_newton_actuators", False): + # Newton fast path: pos/vel targets pass straight through; ``joint_f_2d`` already + # merges Newton's explicit-DOF output with user feedforward. + user_effort = articulation._physx_actuator_wrapper.joint_f_2d + user_pos_target = collection._joint_pos_target + user_vel_target = collection._joint_vel_target + else: + # Standard Lab actuator path: push the processed staging buffers PhysX-side. + user_effort = collection._joint_effort_target_sim + user_pos_target = collection._joint_pos_target_sim + user_vel_target = collection._joint_vel_target_sim + + if articulation.data.has_joint_ordering: + ordering_kernels.launch_reorder_joint_targets_user_to_backend( + user_effort=user_effort, + user_pos_target=user_pos_target, + user_vel_target=user_vel_target, + backend_to_user=articulation.data.joint_ordering.backend_to_user, + write_effort=True, + write_pos_target=articulation._has_implicit_actuators, + write_vel_target=articulation._has_implicit_actuators, + write_joint_act=False, + backend_effort=articulation._joint_effort_target_backend, + backend_pos_target=articulation._joint_pos_target_backend, + backend_vel_target=articulation._joint_vel_target_backend, + backend_joint_act=None, + device=self.device, + ) + effort_target = articulation._joint_effort_target_backend + pos_target = articulation._joint_pos_target_backend + vel_target = articulation._joint_vel_target_backend + else: + effort_target = user_effort + pos_target = user_pos_target + vel_target = user_vel_target + + articulation.root_view.set_dof_actuation_forces(effort_target, articulation._ALL_INDICES) + if articulation._has_implicit_actuators: + articulation.root_view.set_dof_position_targets(pos_target, articulation._ALL_INDICES) + articulation.root_view.set_dof_velocity_targets(vel_target, articulation._ALL_INDICES) + + def reset_native_actuators(self, env_ids: Sequence[int] | slice) -> None: + if self._native_actuator_path_active and self._actuator_runtime is not None: + self._actuator_runtime.reset(env_ids) diff --git a/source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation.py b/source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation.py index b482704fff91..4cc47f75416d 100644 --- a/source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation.py +++ b/source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation.py @@ -8,7 +8,6 @@ from __future__ import annotations -import importlib.util import logging import warnings from collections.abc import Sequence @@ -21,28 +20,23 @@ from pxr import UsdPhysics -from isaaclab.actuators import ActuatorBase, ActuatorBaseCfg, ImplicitActuator +from isaaclab.actuators import ActuatorCollection from isaaclab.assets.articulation import ordering_kernels from isaaclab.assets.articulation.base_articulation import BaseArticulation -from isaaclab.sim.utils.queries import find_first_matching_prim, path_expr_to_glob, resolve_matching_prims_from_source +from isaaclab.sim.utils.queries import path_expr_to_glob, resolve_matching_prims_from_source from isaaclab.utils.string import resolve_matching_names, resolve_matching_names_values -from isaaclab.utils.types import ArticulationActions from isaaclab.utils.version import get_isaac_sim_version, has_kit from isaaclab.utils.warp import ProxyArray from isaaclab.utils.wrench_composer import WrenchComposer -_HAS_NEWTON_ACTUATORS = importlib.util.find_spec("isaaclab_newton.actuators") is not None - - from isaaclab_physx.assets import kernels as shared_kernels from isaaclab_physx.assets.articulation import kernels as articulation_kernels from isaaclab_physx.physics import PhysxManager as SimulationManager +from .actuator_control import PhysxActuatorControl from .articulation_data import ArticulationData if TYPE_CHECKING: - from isaaclab_newton.actuators import NewtonActuatorAdapter - import omni.physics.tensors as physx from isaaclab.assets.articulation.articulation_cfg import ArticulationCfg @@ -230,17 +224,12 @@ def reset(self, env_ids: Sequence[int] | None = None, env_mask: wp.array | None env_ids: Environment indices. If None, then all indices are used. env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). """ - if isinstance(env_ids, slice) and env_ids == slice(None): - env_ids = None - # reset actuators (including Newton-native adapter which owns its states) - for actuator in self.actuators.values(): - actuator.reset(env_ids) - # Reset Newton-actuator per-env states (delay queues, neural hidden state, etc.). - # The adapter is per-articulation on PhysX and is not part of ``self.actuators``. - # ``getattr`` guards subclasses (e.g. ``Multirotor``) that override - # ``_process_actuators_cfg`` and never initialize these attributes. - if getattr(self, "_has_newton_actuators", False) and getattr(self, "newton_actuator_adapter", None) is not None: - self.newton_actuator_adapter.reset(env_ids) + # use ellipses object to skip initial indices. + if (env_ids is None) or (env_ids == slice(None)): + env_ids = slice(None) + # reset actuators, including backend-native actuator state. None selects all + # environments; delayed-actuator buffers do not accept a slice. + self.actuators.reset(None if env_ids == slice(None) else env_ids) # reset external wrenches. self._instantaneous_wrench_composer.reset(env_ids, env_mask) self._permanent_wrench_composer.reset(env_ids, env_mask) @@ -292,60 +281,10 @@ def write_data_to_sim(self): if self._instantaneous_wrench_composer.active: self._instantaneous_wrench_composer.reset() - if getattr(self, "_has_newton_actuators", False): - # Newton fast path: pos/vel targets pass straight through; the - # in-graph kernel inside ``_apply_actuator_model_newton`` merges - # Newton's actuator output (explicit DOFs) with user FF - # (implicit DOFs) into ``w.joint_f_2d``, which is what we push - # to PhysX as the actuation force. - self._apply_actuator_model_newton() - user_effort = self._physx_actuator_wrapper.joint_f_2d - user_pos_target = self._data._joint_pos_target - user_vel_target = self._data._joint_vel_target - else: - # Standard Lab actuator path: per-group ``actuator.compute()`` may - # transform targets, so we push the staging buffers PhysX-side. - self._apply_actuator_model() - user_effort = self._joint_effort_target_sim - user_pos_target = self._joint_pos_target_sim - user_vel_target = self._joint_vel_target_sim - - if self.data.has_joint_ordering: - # One fused gather replaces the per-target reorder launches. PhysX has no - # direct-drive joint_act output, so its gated-off output is left unset. - wp.launch( - ordering_kernels.reorder_joint_targets_user_to_backend, - dim=(self.num_instances, self.num_joints), - inputs=[ - user_effort, - user_pos_target, - user_vel_target, - self.data.joint_ordering.backend_to_user, - True, - self._has_implicit_actuators, - self._has_implicit_actuators, - False, - ], - outputs=[ - self._joint_effort_target_backend, - self._joint_pos_target_backend, - self._joint_vel_target_backend, - None, - ], - device=self.device, - ) - effort_target = self._joint_effort_target_backend - pos_target = self._joint_pos_target_backend - vel_target = self._joint_vel_target_backend - else: - effort_target = user_effort - pos_target = user_pos_target - vel_target = user_vel_target - - self.root_view.set_dof_actuation_forces(effort_target, self._ALL_INDICES) - if self._has_implicit_actuators: - self.root_view.set_dof_position_targets(pos_target, self._ALL_INDICES) - self.root_view.set_dof_velocity_targets(vel_target, self._ALL_INDICES) + # Compute processed actuator commands (native path is a no-op here) and + # submit them to the backend through the collection's control adapter. + self.actuators.compute(SimulationManager.get_physics_dt()) + self.actuators.submit_commands() def update(self, dt: float): """Updates the simulation data. @@ -1551,22 +1490,21 @@ def write_actuator_stiffness_to_sim( env_ids: torch.Tensor, joint_ids: torch.Tensor, ) -> None: - """Write actuator kp at the (env_ids, joint_ids) sub-grid and propagate to controllers. + """Write native actuator stiffness [N/m or N·m/rad, depending on joint type]. - Iterates the per-articulation adapter's Newton actuators and uses - :data:`patch_actuator_param_kernel` to overwrite each - controller's ``kp`` array at the ``(env_ids × joint_ids)`` - cells. DOFs not owned by an actuator are skipped by the kernel's - per-slot index mapping. + .. deprecated:: 3.0 + Use :func:`isaaclab.envs.mdp.events.randomize_actuator_gains` for + managed randomization. Direct controller-gain writes have no public + replacement. This method will be removed in 4.0. Args: - stiffness: Sub-grid of new kp values, shape ``(len(env_ids), len(joint_ids))``. - env_ids: 1D torch tensor of env indices. - joint_ids: 1D torch tensor of articulation-local joint indices. - - No-op when no Newton actuators are registered for this articulation. + stiffness: Controller stiffness [N/m or N·m/rad, depending on joint type]. + env_ids: Articulation instance indices. + joint_ids: Articulation-local joint indices. """ - self._write_actuator_param("kp", stiffness, env_ids, joint_ids) + self._write_deprecated_native_actuator_gain( + "write_actuator_stiffness_to_sim", "kp", stiffness, env_ids, joint_ids + ) def write_actuator_damping_to_sim( self, @@ -1575,72 +1513,19 @@ def write_actuator_damping_to_sim( env_ids: torch.Tensor, joint_ids: torch.Tensor, ) -> None: - """Write actuator kd at the (env_ids, joint_ids) sub-grid and propagate to controllers.""" - self._write_actuator_param("kd", damping, env_ids, joint_ids) + """Write native actuator damping [N·s/m or N·m·s/rad, depending on joint type]. - def _write_actuator_param( - self, - attr: str, - values: torch.Tensor, - env_ids: torch.Tensor, - joint_ids: torch.Tensor, - ) -> None: - """Shared body for :meth:`write_actuator_stiffness_to_sim` / :meth:`write_actuator_damping_to_sim`.""" - adapter = self.newton_actuator_adapter - if adapter is None: - return + .. deprecated:: 3.0 + Use :func:`isaaclab.envs.mdp.events.randomize_actuator_gains` for + managed randomization. Direct controller-gain writes have no public + replacement. This method will be removed in 4.0. - from isaaclab_newton.actuators import kernels as actuator_kernels # noqa: PLC0415 - - env_id_pos = torch.full( - (self.num_instances,), - -1, - dtype=torch.int32, - device=self.device, - ) - env_id_pos[env_ids.to(self.device, dtype=torch.long)] = torch.arange( - env_ids.shape[0], - dtype=torch.int32, - device=self.device, - ) - joint_id_pos = torch.full( - (self.num_joints,), - -1, - dtype=torch.int32, - device=self.device, - ) - joint_ids_local = joint_ids.to(self.device, dtype=torch.long) - joint_id_pos[joint_ids_local] = torch.arange( - joint_ids.shape[0], - dtype=torch.int32, - device=self.device, - ) - - values_wp = wp.from_torch( - values.to(self.device, dtype=torch.float32).contiguous(), - dtype=wp.float32, - ) - env_id_pos_wp = wp.from_torch(env_id_pos, dtype=wp.int32) - joint_id_pos_wp = wp.from_torch(joint_id_pos, dtype=wp.int32) - - for act in adapter.actuators: - ctrl = act.controller - if not hasattr(ctrl, attr): - continue - wp.launch( - actuator_kernels.patch_actuator_param_kernel, - dim=act.indices.shape[0], - inputs=[ - act.indices, - env_id_pos_wp, - joint_id_pos_wp, - values_wp, - 0, - self.num_joints, - ], - outputs=[getattr(ctrl, attr)], - device=self.device, - ) + Args: + damping: Controller damping [N·s/m or N·m·s/rad, depending on joint type]. + env_ids: Articulation instance indices. + joint_ids: Articulation-local joint indices. + """ + self._write_deprecated_native_actuator_gain("write_actuator_damping_to_sim", "kd", damping, env_ids, joint_ids) def write_joint_damping_to_sim_mask( self, @@ -2809,240 +2694,6 @@ def set_inertias_mask( # Set full data to True to ensure the right code path is taken inside the kernel. self.set_inertias_index(inertias=inertias, body_ids=body_ids, env_ids=env_ids, full_data=True) - def set_joint_position_target_index( - self, - *, - target: torch.Tensor | wp.array, - joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - full_data: bool = False, - ) -> None: - """Set joint position targets into internal buffers using indices. - - This function does not apply the joint targets to the simulation. It only fills the buffers with - the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. - - .. note:: - This method expects partial data or full data. - - .. tip:: - For maximum performance we recommend using the index method. This is because in PhysX, the tensor API - is only supporting indexing, hence masks need to be converted to indices. - - Args: - target: Joint position targets. Shape is (len(env_ids), len(joint_ids)) or (num_instances, num_joints) - if full_data. - joint_ids: The joint indices to set the targets for. Defaults to None (all joints). - env_ids: The environment indices to set the targets for. Defaults to None (all environments). - full_data: Whether to expect full data. Defaults to False. - """ - # resolve all indices - env_ids = self._resolve_env_ids(env_ids) - joint_ids = self._resolve_joint_ids(joint_ids) - if full_data: - self.assert_shape_and_dtype(target, (self.num_instances, self.num_joints), wp.float32, "target") - else: - self.assert_shape_and_dtype(target, (env_ids.shape[0], joint_ids.shape[0]), wp.float32, "target") - # Warp kernels can ingest torch tensors directly, so we don't need to convert to warp arrays here. - wp.launch( - shared_kernels.write_2d_data_to_buffer_with_indices_kernel(env_ids, joint_ids), - dim=(env_ids.shape[0], joint_ids.shape[0]), - inputs=[ - target, - env_ids, - joint_ids, - full_data, - ], - outputs=[ - self.data._joint_pos_target, - ], - device=self.device, - ) - # Only updates internal buffers, does not apply the targets to the simulation. - - def set_joint_position_target_mask( - self, - *, - target: torch.Tensor | wp.array, - joint_mask: wp.array | None = None, - env_mask: wp.array | None = None, - ) -> None: - """Set joint position targets into internal buffers using masks. - - .. note:: - This method expects full data. - - .. tip:: - For maximum performance we recommend using the index method. This is because in PhysX, the tensor API - is only supporting indexing, hence masks need to be converted to indices. - - Args: - target: Joint position targets. Shape is (num_instances, num_joints). - joint_mask: Joint mask. If None, then all joints are used. - env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). - """ - # Resolve masks. - env_ids = self._resolve_env_mask(env_mask) - joint_ids = self._resolve_joint_mask(joint_mask) - # Set full data to True to ensure the right code path is taken inside the kernel. - self.set_joint_position_target_index(target=target, joint_ids=joint_ids, env_ids=env_ids, full_data=True) - - def set_joint_velocity_target_index( - self, - *, - target: torch.Tensor | wp.array, - joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - full_data: bool = False, - ) -> None: - """Set joint velocity targets into internal buffers using indices. - - This function does not apply the joint targets to the simulation. It only fills the buffers with - the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. - - .. note:: - This method expects partial data or full data. - - .. tip:: - For maximum performance we recommend using the index method. This is because in PhysX, the tensor API - is only supporting indexing, hence masks need to be converted to indices. - - Args: - target: Joint velocity targets. Shape is (len(env_ids), len(joint_ids)) or (num_instances, num_joints) - if full_data. - joint_ids: The joint indices to set the targets for. Defaults to None (all joints). - env_ids: The environment indices to set the targets for. Defaults to None (all environments). - full_data: Whether to expect full data. Defaults to False. - """ - # resolve all indices - env_ids = self._resolve_env_ids(env_ids) - joint_ids = self._resolve_joint_ids(joint_ids) - if full_data: - self.assert_shape_and_dtype(target, (self.num_instances, self.num_joints), wp.float32, "target") - else: - self.assert_shape_and_dtype(target, (env_ids.shape[0], joint_ids.shape[0]), wp.float32, "target") - # Warp kernels can ingest torch tensors directly, so we don't need to convert to warp arrays here. - wp.launch( - shared_kernels.write_2d_data_to_buffer_with_indices_kernel(env_ids, joint_ids), - dim=(env_ids.shape[0], joint_ids.shape[0]), - inputs=[ - target, - env_ids, - joint_ids, - full_data, - ], - outputs=[ - self.data._joint_vel_target, - ], - device=self.device, - ) - # Only updates internal buffers, does not apply the targets to the simulation. - - def set_joint_velocity_target_mask( - self, - *, - target: torch.Tensor | wp.array, - joint_mask: wp.array | None = None, - env_mask: wp.array | None = None, - ) -> None: - """Set joint velocity targets into internal buffers using masks. - - .. note:: - This method expects partial data or full data. - - .. tip:: - For maximum performance we recommend using the index method. This is because in PhysX, the tensor API - is only supporting indexing, hence masks need to be converted to indices. - - Args: - target: Joint velocity targets. Shape is (num_instances, num_joints). - joint_mask: Joint mask. If None, then all joints are used. - env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). - """ - # Resolve masks. - env_ids = self._resolve_env_mask(env_mask) - joint_ids = self._resolve_joint_mask(joint_mask) - # Set full data to True to ensure the right code path is taken inside the kernel. - self.set_joint_velocity_target_index(target=target, joint_ids=joint_ids, env_ids=env_ids, full_data=True) - - def set_joint_effort_target_index( - self, - *, - target: torch.Tensor | wp.array, - joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, - full_data: bool = False, - ) -> None: - """Set joint efforts into internal buffers using indices. - - This function does not apply the joint targets to the simulation. It only fills the buffers with - the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. - - .. note:: - This method expects partial data or full data. - - .. tip:: - For maximum performance we recommend using the index method. This is because in PhysX, the tensor API - is only supporting indexing, hence masks need to be converted to indices. - - Args: - target: Joint effort targets. Shape is (len(env_ids), len(joint_ids)) or (num_instances, num_joints) - if full_data. - joint_ids: The joint indices to set the targets for. Defaults to None (all joints). - env_ids: The environment indices to set the targets for. Defaults to None (all environments). - full_data: Whether to expect full data. Defaults to False. - """ - # resolve all indices - env_ids = self._resolve_env_ids(env_ids) - joint_ids = self._resolve_joint_ids(joint_ids) - if full_data: - self.assert_shape_and_dtype(target, (self.num_instances, self.num_joints), wp.float32, "target") - else: - self.assert_shape_and_dtype(target, (env_ids.shape[0], joint_ids.shape[0]), wp.float32, "target") - # Warp kernels can ingest torch tensors directly, so we don't need to convert to warp arrays here. - wp.launch( - shared_kernels.write_2d_data_to_buffer_with_indices_kernel(env_ids, joint_ids), - dim=(env_ids.shape[0], joint_ids.shape[0]), - inputs=[ - target, - env_ids, - joint_ids, - full_data, - ], - outputs=[ - self.data._joint_effort_target, - ], - device=self.device, - ) - # Only updates internal buffers, does not apply the targets to the simulation. - - def set_joint_effort_target_mask( - self, - *, - target: torch.Tensor | wp.array, - joint_mask: wp.array | None = None, - env_mask: wp.array | None = None, - ) -> None: - """Set joint efforts into internal buffers using masks. - - .. note:: - This method expects full data. - - .. tip:: - For maximum performance we recommend using the index method. This is because in PhysX, the tensor API - is only supporting indexing, hence masks need to be converted to indices. - - Args: - target: Joint effort targets. Shape is (num_instances, num_joints). - joint_mask: Joint mask. If None, then all joints are used. - env_mask: Environment mask. If None, then all the instances are updated. Shape is (num_instances,). - """ - # Resolve masks. - env_ids = self._resolve_env_mask(env_mask) - joint_ids = self._resolve_joint_mask(joint_mask) - # Set full data to True to ensure the right code path is taken inside the kernel. - self.set_joint_effort_target_index(target=target, joint_ids=joint_ids, env_ids=env_ids, full_data=True) - """ Operations - Tendons. """ @@ -4277,11 +3928,6 @@ def _create_buffers(self): self._ordering_configure_backend_staging() # tendon names are set in _process_tendons function - # -- joint commands (sent to the simulation after actuator processing) - self._joint_pos_target_sim = wp.zeros_like(self.data.joint_pos_target, device=self.device) - self._joint_vel_target_sim = wp.zeros_like(self.data.joint_pos_target, device=self.device) - self._joint_effort_target_sim = wp.zeros_like(self.data.joint_pos_target, device=self.device) - # soft joint position limits (recommended not to be too close to limits). wp.launch( articulation_kernels.update_soft_joint_pos_limits, @@ -4392,272 +4038,17 @@ def _get_user_ordered_joint_3d_buffer( """ def _process_actuators_cfg(self): - """Process and apply articulation joint properties.""" - # create actuators - self.actuators = dict() - self._physx_actuator_wrapper = None - # Per-articulation Newton actuator adapter and the frozen kp/kd - # snapshot consumed by ``randomize_actuator_gains``. ``None`` for - # articulations not on the Newton fast path or with only implicit - # Lab actuators. - self.newton_actuator_adapter: NewtonActuatorAdapter | None = None - self.newton_default_stiffness: torch.Tensor | None = None - self.newton_default_damping: torch.Tensor | None = None - self.newton_managed_local_joints: torch.Tensor | slice | None = None - # flag for implicit actuators - # if this is false, we by-pass certain checks when doing actuator-related operations - self._has_implicit_actuators = False - self._has_newton_actuators = False - # Per-DOF implicit/explicit mask consumed by the - # ``sync_torque_telemetry`` kernel. ``None`` when no Newton fast path - # is active. - self._implicit_dof_mask: wp.array | None = None - - _use_newton_actuators = getattr(self._sim_cfg, "use_newton_actuators", False) - - if _use_newton_actuators and not _HAS_NEWTON_ACTUATORS: - logger.warning( - "use_newton_actuators is enabled but 'isaaclab_newton.actuators' is not available." - " Newton-native actuators will be disabled and the simulation will fall back to the" - " Isaac Lab actuator path. Install the isaaclab_newton extension to enable the fast path." - ) - - if _HAS_NEWTON_ACTUATORS and _use_newton_actuators: - from isaaclab_newton.actuators import ( # noqa: PLC0415 - NewtonActuatorAdapter, - PhysxActuatorWrapper, - build_implicit_dof_mask, - ) - - from isaaclab.sim.utils.stage import get_current_stage # noqa: PLC0415 - - # Enable the fast path even for all-implicit articulations: - # PhysX runs PD internally; Lab only forwards targets. - self._has_newton_actuators = True - - # Author Newton actuator prims only if any explicit Lab group exists. - has_explicit = any( - not ( - "ImplicitActuator" in actuator_cfg.class_type - if isinstance(actuator_cfg.class_type, str) - else issubclass(actuator_cfg.class_type, ImplicitActuator) - ) - for actuator_cfg in self.cfg.actuators.values() - ) - - # Always allocate the wrapper so ``_apply_actuator_model_newton`` - # has a ``joint_f_2d`` buffer to merge effort into, even when - # there are no explicit Newton actuators (implicit-only case). - self._physx_actuator_wrapper = PhysxActuatorWrapper.create( - num_envs=self.num_instances, - num_joints=self.num_joints, - device=self.device, - ) - - if has_explicit: - first_prim = find_first_matching_prim(self.cfg.prim_path) - art_prim_path = str(first_prim.GetPath()) if first_prim is not None else None - - adapter = NewtonActuatorAdapter.from_usd( - stage=get_current_stage(), - joint_names=self.joint_names, - num_envs=self.num_instances, - num_joints=self.num_joints, - device=self.device, - articulation_prim_path=art_prim_path, - ) - - # Bind the wrapper's flat aliases of state/input buffers once. - # The underlying wp.arrays alias stable PhysX-owned GPU memory - # whose device pointer is fixed for the articulation's lifetime, - # so the views remain valid for every subsequent step. - w = self._physx_actuator_wrapper - w.joint_q = self._data.joint_pos.warp.reshape(-1) - w.joint_qd = self._data.joint_vel.warp.reshape(-1) - w.joint_target_q = self._data.joint_pos_target.warp.reshape(-1) - w.joint_target_qd = self._data.joint_vel_target.warp.reshape(-1) - w.joint_target_pos = self._data.joint_pos_target.warp.reshape(-1) - w.joint_target_vel = self._data.joint_vel_target.warp.reshape(-1) - w.joint_act = self._data.joint_effort_target.warp.reshape(-1) - adapter.finalize(w) - self.newton_actuator_adapter = adapter - self.write_joint_stiffness_to_sim_index(stiffness=0.0, joint_ids=adapter.joint_indices) - self.write_joint_damping_to_sim_index(damping=0.0, joint_ids=adapter.joint_indices) - - for actuator_name, actuator_cfg in self.cfg.actuators.items(): - cls_type = actuator_cfg.class_type - is_implicit = ( - "ImplicitActuator" in cls_type - if isinstance(cls_type, str) - else issubclass(cls_type, ImplicitActuator) - ) - if is_implicit: - self._create_lab_actuator(actuator_name, actuator_cfg) - else: - self._create_lab_actuator(actuator_name, actuator_cfg, properties_only=True) - - # Bind this articulation to its Newton adapter: one call snapshots - # the initial gains, builds the implicit-DOF mask, and takes the - # per-articulation computed-effort view that ``_apply_actuator_model_newton`` - # passes straight to ``sync_torque_telemetry``. ``_implicit_dof_mask_owner`` - # is retained as an instance attribute so the torch tensor backing - # ``_implicit_dof_mask`` isn't freed while a captured CUDA graph holds - # a pointer into it. Falls back to a freshly built mask and a zero - # computed-effort buffer when there are no explicit Newton actuators — - # the kernel only reads the buffer on explicit DOFs. - if self.newton_actuator_adapter is not None: - binding = self.newton_actuator_adapter.bind_articulation( - lab_actuators=self.actuators, - dof_offset=0, - num_joints=self.num_joints, - ) - self.newton_default_stiffness = binding.stiffness - self.newton_default_damping = binding.damping - self.newton_managed_local_joints = binding.joint_indices - self._implicit_dof_mask = binding.implicit_dof_mask - self._implicit_dof_mask_owner = binding.implicit_dof_mask_owner - self._data._sim_bind_joint_computed_effort = binding.computed_effort_view - else: - self._implicit_dof_mask, self._implicit_dof_mask_owner = build_implicit_dof_mask( - self.actuators, - self.num_joints, - self.device, - ) - self._data._sim_bind_joint_computed_effort = wp.zeros( - (self.num_instances, self.num_joints), - dtype=wp.float32, - device=self.device, - ) - return - - # --- Standard Isaac Lab actuator path --- - for actuator_name, actuator_cfg in self.cfg.actuators.items(): - self._create_lab_actuator(actuator_name, actuator_cfg) - - # perform some sanity checks to ensure actuators are prepared correctly - total_act_joints = sum(actuator.num_joints for actuator in self.actuators.values()) - if total_act_joints != (self.num_joints - self.num_fixed_tendons): - logger.warning( - "Not all actuators are configured! Total number of actuated joints not equal to number of" - f" joints available: {total_act_joints} != {self.num_joints - self.num_fixed_tendons}." - ) - - if self.cfg.actuator_value_resolution_debug_print: - if _HAS_NEWTON_ACTUATORS: - from isaaclab_newton.actuators import NewtonActuatorAdapter # noqa: PLC0415 - else: - NewtonActuatorAdapter = None # type: ignore[assignment] - t = PrettyTable(["Group", "Property", "Name", "ID", "USD Value", "ActutatorCfg Value", "Applied"]) - for actuator_group, actuator in self.actuators.items(): - if NewtonActuatorAdapter is not None and isinstance(actuator, NewtonActuatorAdapter): - continue - group_count = 0 - for property, resolution_details in actuator.joint_property_resolution_table.items(): - for prop_idx, resolution_detail in enumerate(resolution_details): - actuator_group_str = actuator_group if group_count == 0 else "" - property_str = property if prop_idx == 0 else "" - fmt = [f"{v:.2e}" if isinstance(v, float) else str(v) for v in resolution_detail] - t.add_row([actuator_group_str, property_str, *fmt]) - group_count += 1 - logger.warning(f"\nActuatorCfg-USD Value Discrepancy Resolution (matching values are skipped): \n{t}") - - def _create_lab_actuator( - self, - actuator_name: str, - actuator_cfg: ActuatorBaseCfg, - *, - properties_only: bool = False, - ) -> None: - """Instantiate a single Lab actuator from its config and write properties to sim. - - Args: - actuator_name: Name for the actuator group. - actuator_cfg: Configuration for the actuator. - properties_only: When ``True``, only write physical joint properties - (armature, limits, friction) without registering the actuator or - writing stiffness/damping. Used for explicit joints managed by - Newton actuators. - """ - joint_ids, joint_names = self.find_joints(actuator_cfg.joint_names_expr, as_proxy=True) - if len(joint_names) == 0: - raise ValueError( - f"No joints found for actuator group: {actuator_name} with joint name expression:" - f" {actuator_cfg.joint_names_expr}." - ) - joint_ids = slice(None) if joint_names == self.joint_names else joint_ids.torch - torch_joint_ids = joint_ids - - actuator: ActuatorBase = actuator_cfg.class_type( - cfg=actuator_cfg, - joint_names=joint_names, - joint_ids=joint_ids, - num_envs=self.num_instances, - device=self.device, - stiffness=wp.to_torch(self._data.joint_stiffness)[:, torch_joint_ids], - damping=wp.to_torch(self._data.joint_damping)[:, torch_joint_ids], - armature=wp.to_torch(self._data.joint_armature)[:, torch_joint_ids], - friction=wp.to_torch(self._data.joint_friction_coeff)[:, torch_joint_ids], - dynamic_friction=wp.to_torch(self._data.joint_dynamic_friction_coeff)[:, torch_joint_ids], - viscous_friction=wp.to_torch(self._data.joint_viscous_friction_coeff)[:, torch_joint_ids], - effort_limit=wp.to_torch(self._data.joint_effort_limits)[:, torch_joint_ids].clone(), - velocity_limit=wp.to_torch(self._data.joint_vel_limits)[:, torch_joint_ids], + """Process actuator configs through :class:`ActuatorCollection`.""" + self._actuator_control = PhysxActuatorControl(self) + self.actuators = ActuatorCollection( + self.cfg.actuators, + self._actuator_control, + debug_value_resolution=self.cfg.actuator_value_resolution_debug_print, ) - - # Write physical joint properties (armature, limits, friction) — always needed. - self.write_joint_effort_limit_to_sim_index( - limits=actuator.effort_limit_sim, - joint_ids=actuator.joint_indices, - ) - self.write_joint_velocity_limit_to_sim_index( - limits=actuator.velocity_limit_sim, - joint_ids=actuator.joint_indices, - ) - self.write_joint_armature_to_sim_index(armature=actuator.armature, joint_ids=actuator.joint_indices) - self.write_joint_friction_coefficient_to_sim_index( - joint_friction_coeff=actuator.friction, - joint_ids=actuator.joint_indices, - ) - self.write_joint_dynamic_friction_coefficient_to_sim_index( - joint_dynamic_friction_coeff=actuator.dynamic_friction, - joint_ids=actuator.joint_indices, - ) - self.write_joint_viscous_friction_coefficient_to_sim_index( - joint_viscous_friction_coeff=actuator.viscous_friction, - joint_ids=actuator.joint_indices, - ) - - if properties_only: - return - - self.actuators[actuator_name] = actuator - - # Store the configured values from the actuator model - j_ids = actuator.joint_indices - if isinstance(j_ids, slice): - j_ids = self._ALL_JOINT_INDICES - for attr, buf in ( - (actuator.stiffness, self.data._joint_stiffness), - (actuator.damping, self.data._joint_damping), - (actuator.armature, self.data._joint_armature), - (actuator.friction, self.data._joint_friction_coeff), - (actuator.dynamic_friction, self.data._joint_dynamic_friction_coeff), - (actuator.viscous_friction, self.data._joint_viscous_friction_coeff), - ): - wp.launch( - shared_kernels.write_2d_data_to_buffer_with_indices_kernel(self._ALL_INDICES, j_ids), - dim=(self.num_instances, j_ids.shape[0]), - inputs=[attr, self._ALL_INDICES, j_ids, False], - outputs=[buf], - device=self.device, - ) - - if isinstance(actuator, ImplicitActuator): - self._has_implicit_actuators = True - self.write_joint_stiffness_to_sim_index(stiffness=actuator.stiffness, joint_ids=actuator.joint_indices) - self.write_joint_damping_to_sim_index(damping=actuator.damping, joint_ids=actuator.joint_indices) - else: - self.write_joint_stiffness_to_sim_index(stiffness=0.0, joint_ids=actuator.joint_indices) - self.write_joint_damping_to_sim_index(damping=0.0, joint_ids=actuator.joint_indices) + self._has_implicit_actuators = self.actuators.has_implicit_actuators + self._has_newton_actuators = self._actuator_control.native_actuator_path_active + self._physx_actuator_wrapper = self._actuator_control._physx_actuator_wrapper + self._data.bind_actuator_collection(self.actuators) def _process_tendons(self): """Process fixed and spatial tendons.""" @@ -4686,128 +4077,6 @@ def _process_tendons(self): self._data.fixed_tendon_names = self._fixed_tendon_names self._data.spatial_tendon_names = self._spatial_tendon_names - def _apply_actuator_model(self): - """Processes joint commands for the articulation by forwarding them to the actuators. - - The actions are first processed using actuator models. Depending on the robot configuration, - the actuator models compute the joint level simulation commands and sets them into the PhysX buffers. - """ - # process actions per group - for actuator in self.actuators.values(): - # prepare input for actuator model based on cached data - actuator_joint_indices = actuator.joint_indices - torch_joint_indices = actuator_joint_indices - # TODO : A tensor dict would be nice to do the indexing of all tensors together - control_action = ArticulationActions( - joint_positions=self._data.joint_pos_target.torch[:, torch_joint_indices], - joint_velocities=self._data.joint_vel_target.torch[:, torch_joint_indices], - joint_efforts=self._data.joint_effort_target.torch[:, torch_joint_indices], - joint_indices=torch_joint_indices, - ) - # compute joint command from the actuator model - control_action = actuator.compute( - control_action, - joint_pos=self._data.joint_pos.torch[:, torch_joint_indices], - joint_vel=self._data.joint_vel.torch[:, torch_joint_indices], - ) - # update targets (these are set into the simulation) - joint_indices = actuator_joint_indices - if isinstance(joint_indices, slice) or joint_indices is None: - joint_indices = self._ALL_JOINT_INDICES - if hasattr(actuator, "gear_ratio"): - gear_ratio = actuator.gear_ratio - else: - gear_ratio = None - wp.launch( - articulation_kernels.update_targets, - dim=(self.num_instances, joint_indices.shape[0]), - inputs=[ - control_action.joint_positions, - control_action.joint_velocities, - control_action.joint_efforts, - joint_indices, - ], - outputs=[ - self._joint_pos_target_sim, - self._joint_vel_target_sim, - self._joint_effort_target_sim, - ], - device=self.device, - ) - # update state of the actuator model - wp.launch( - articulation_kernels.update_actuator_state_model, - dim=(self.num_instances, joint_indices.shape[0]), - inputs=[ - actuator.computed_effort, - actuator.applied_effort, - gear_ratio, - actuator.velocity_limit, - joint_indices, - ], - outputs=[ - self._data.computed_torque, - self._data.applied_torque, - self._data.gear_ratio, - self._data.soft_joint_vel_limits, - ], - device=self.device, - ) - - def _apply_actuator_model_newton(self): - """Pre-fill effort buffer with FF, step Newton actuators, sync telemetry. - - Pre-fills ``w.joint_f_2d`` with the user's effort target across all - DOFs. ``newton_adapter.step`` (no-op if no explicit Newton actuators - exist) then zeroes ``joint_f_2d`` at explicit DOFs and overwrites - them with each actuator's computed effort, while implicit DOFs keep - the FF. The :func:`sync_torque_telemetry` kernel then fills - ``_data._computed_torque`` / ``_data._applied_torque`` from the - resulting buffer. The final ``joint_f_2d`` is what gets pushed to - PhysX as the actuation force in :meth:`write_data_to_sim`. - """ - from isaaclab_newton.actuators import kernels as actuator_kernels # noqa: PLC0415 - - w = self._physx_actuator_wrapper - w.joint_f_2d.assign(self._data._joint_effort_target) - if self.newton_actuator_adapter is not None: - if self.data.has_joint_ordering: - # ``w.joint_q``/``w.joint_qd`` were bound once (at actuator setup) to - # ``_data.joint_pos``/``_data.joint_vel``. With identity ordering those - # bindings alias PhysX-owned memory directly and are always current. With - # non-identity ordering they alias an owned shadow buffer that is only - # refreshed when the public ``joint_pos``/``joint_vel`` getters run -- which - # otherwise would not happen until the telemetry kernel below reads them, - # one step too late for the adapter. Force the refresh here so the adapter - # sees this step's state instead of a stale one-step-old shadow. - self._data._refresh_joint_pos() - self._data._refresh_joint_vel() - self.newton_actuator_adapter.step(w, w, SimulationManager.get_physics_dt()) - - wp.launch( - actuator_kernels.sync_torque_telemetry, - dim=(self.num_instances, self.num_joints), - inputs=[ - self._data.joint_pos.warp, - self._data.joint_vel.warp, - self._data._joint_pos_target, - self._data._joint_vel_target, - self._data.joint_stiffness.warp, - self._data.joint_damping.warp, - self._data.joint_effort_limits.warp, - self._implicit_dof_mask, - w.joint_f_2d, - self._data._sim_bind_joint_computed_effort, - self._ALL_JOINT_INDICES, - False, - ], - outputs=[ - self._data._computed_torque, - self._data._applied_torque, - ], - device=self.device, - ) - """ Internal helpers -- Debugging. """ diff --git a/source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation_data.py b/source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation_data.py index 275703440473..c8d776e756b4 100644 --- a/source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation_data.py +++ b/source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation_data.py @@ -379,83 +379,6 @@ def default_joint_vel(self, value: wp.array) -> None: raise ValueError("The articulation data is already primed.") self._default_joint_vel.assign(value) - """ - Joint commands -- Set into simulation. - """ - - @property - def joint_pos_target(self) -> ProxyArray: - """Joint position targets commanded by the user. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - - For an implicit actuator model, the targets are directly set into the simulation. - For an explicit actuator model, the targets are used to compute the joint torques (see :attr:`applied_torque`), - which are then set into the simulation. - """ - if self._joint_pos_target_ta is None: - self._joint_pos_target_ta = ProxyArray(self._joint_pos_target) - return self._joint_pos_target_ta - - @property - def joint_vel_target(self) -> ProxyArray: - """Joint velocity targets commanded by the user. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - - For an implicit actuator model, the targets are directly set into the simulation. - For an explicit actuator model, the targets are used to compute the joint torques (see :attr:`applied_torque`), - which are then set into the simulation. - """ - if self._joint_vel_target_ta is None: - self._joint_vel_target_ta = ProxyArray(self._joint_vel_target) - return self._joint_vel_target_ta - - @property - def joint_effort_target(self) -> ProxyArray: - """Joint effort targets commanded by the user. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - - For an implicit actuator model, the targets are directly set into the simulation. - For an explicit actuator model, the targets are used to compute the joint torques (see :attr:`applied_torque`), - which are then set into the simulation. - """ - if self._joint_effort_target_ta is None: - self._joint_effort_target_ta = ProxyArray(self._joint_effort_target) - return self._joint_effort_target_ta - - """ - Joint commands -- Explicit actuators. - """ - - @property - def computed_torque(self) -> ProxyArray: - """Joint torques computed from the actuator model (before clipping). - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - - This quantity is the raw torque output from the actuator mode, before any clipping is applied. - It is exposed for users who want to inspect the computations inside the actuator model. - For instance, to penalize the learning agent for a difference between the computed and applied torques. - """ - if self._computed_torque_ta is None: - self._computed_torque_ta = ProxyArray(self._computed_torque) - return self._computed_torque_ta - - @property - def applied_torque(self) -> ProxyArray: - """Joint torques applied from the actuator model (after clipping). - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - - These torques are set into the simulation, after clipping the :attr:`computed_torque` based on the - actuator model. - """ - if self._applied_torque_ta is None: - self._applied_torque_ta = ProxyArray(self._applied_torque) - return self._applied_torque_ta - """ Joint properties """ @@ -594,29 +517,6 @@ def soft_joint_pos_limits(self) -> ProxyArray: self._soft_joint_pos_limits_ta = ProxyArray(self._soft_joint_pos_limits) return self._soft_joint_pos_limits_ta - @property - def soft_joint_vel_limits(self) -> ProxyArray: - """Soft joint velocity limits for all joints. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - - These are obtained from the actuator model. It may differ from :attr:`joint_vel_limits` if the actuator model - has a variable velocity limit model. For instance, in a variable gear ratio actuator model. - """ - if self._soft_joint_vel_limits_ta is None: - self._soft_joint_vel_limits_ta = ProxyArray(self._soft_joint_vel_limits) - return self._soft_joint_vel_limits_ta - - @property - def gear_ratio(self) -> ProxyArray: - """Gear ratio for relating motor torques to applied Joint torques. - - Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to (num_instances, num_joints). - """ - if self._gear_ratio_ta is None: - self._gear_ratio_ta = ProxyArray(self._gear_ratio) - return self._gear_ratio_ta - """ Fixed tendon properties. """ @@ -1859,12 +1759,6 @@ def _create_buffers(self) -> None: self._previous_joint_vel = wp.clone(self._root_view.get_dof_velocities(), device=self.device) # Pre-allocated buffers - # -- Joint commands (set into simulation) - self._joint_pos_target = wp.zeros((self._num_instances, self._num_joints), dtype=wp.float32, device=self.device) - self._joint_vel_target = wp.zeros((self._num_instances, self._num_joints), dtype=wp.float32, device=self.device) - self._joint_effort_target = wp.zeros( - (self._num_instances, self._num_joints), dtype=wp.float32, device=self.device - ) # -- Joint commands (explicit actuator model) self._computed_torque = wp.zeros((self._num_instances, self._num_joints), dtype=wp.float32, device=self.device) self._applied_torque = wp.zeros((self._num_instances, self._num_joints), dtype=wp.float32, device=self.device) @@ -1914,7 +1808,6 @@ def _create_buffers(self) -> None: self._soft_joint_vel_limits = wp.zeros( (self._num_instances, self._num_joints), dtype=wp.float32, device=self.device ) - self._gear_ratio = wp.ones((self._num_instances, self._num_joints), dtype=wp.float32, device=self.device) # -- Fixed tendon properties if self._num_fixed_tendons > 0: self._fixed_tendon_stiffness = wp.clone(self._root_view.get_fixed_tendon_stiffnesses(), device=self.device) @@ -2146,7 +2039,6 @@ def _pin_proxy_arrays(self) -> None: # Joint properties (custom) self._soft_joint_pos_limits_ta: ProxyArray | None = None self._soft_joint_vel_limits_ta: ProxyArray | None = None - self._gear_ratio_ta: ProxyArray | None = None # Fixed tendon properties self._fixed_tendon_stiffness_ta: ProxyArray | None = None self._fixed_tendon_damping_ta: ProxyArray | None = None diff --git a/source/isaaclab_physx/isaaclab_physx/assets/articulation/kernels.py b/source/isaaclab_physx/isaaclab_physx/assets/articulation/kernels.py index f0c8101ea0c1..769914799550 100644 --- a/source/isaaclab_physx/isaaclab_physx/assets/articulation/kernels.py +++ b/source/isaaclab_physx/isaaclab_physx/assets/articulation/kernels.py @@ -541,52 +541,6 @@ def update_targets( target_joint_efforts[i, joint_indices[j]] = source_joint_efforts[i, j] -@wp.kernel -def update_actuator_state_model( - source_computed_effort: wp.array2d(dtype=wp.float32), - source_applied_effort: wp.array2d(dtype=wp.float32), - source_gear_ratio: wp.array2d(dtype=wp.float32), - source_vel_limits: wp.array2d(dtype=wp.float32), - joint_indices: wp.array(dtype=wp.int32), - target_computed_effort: wp.array2d(dtype=wp.float32), - target_applied_effort: wp.array2d(dtype=wp.float32), - target_gear_ratio: wp.array2d(dtype=wp.float32), - target_soft_joint_vel_limits: wp.array2d(dtype=wp.float32), -): - """Update actuator state model parameters from source arrays using joint indices. - - This kernel copies actuator state model parameters (computed effort, applied effort, - gear ratio, and velocity limits) from source arrays to target arrays, remapping - joint indices using the provided joint_indices array. - - Args: - source_computed_effort: Input array of source computed effort values. Shape is - (num_envs, num_selected_joints). - source_applied_effort: Input array of source applied effort values. Shape is - (num_envs, num_selected_joints). - source_gear_ratio: Input array of source gear ratio values. Shape is - (num_envs, num_selected_joints). Can be None if not provided. - source_vel_limits: Input array of source velocity limit values. Shape is - (num_envs, num_selected_joints). - joint_indices: Input array of joint indices for remapping. Shape is - (num_selected_joints,). Specifies which joints in the target arrays to update. - target_computed_effort: Output array where computed effort values are written. - Shape is (num_envs, num_joints). - target_applied_effort: Output array where applied effort values are written. - Shape is (num_envs, num_joints). - target_gear_ratio: Output array where gear ratio values are written. Shape is - (num_envs, num_joints). - target_soft_joint_vel_limits: Output array where soft joint velocity limits are - written. Shape is (num_envs, num_joints). - """ - i, j = wp.tid() - target_computed_effort[i, joint_indices[j]] = source_computed_effort[i, j] - target_applied_effort[i, joint_indices[j]] = source_applied_effort[i, j] - target_soft_joint_vel_limits[i, joint_indices[j]] = source_vel_limits[i, j] - if source_gear_ratio: - target_gear_ratio[i, joint_indices[j]] = source_gear_ratio[i, j] - - @wp.kernel def extract_friction_properties( friction_props: wp.array3d(dtype=wp.float32), diff --git a/source/isaaclab_physx/isaaclab_physx/benchmark/assets/runtime.py b/source/isaaclab_physx/isaaclab_physx/benchmark/assets/runtime.py index cc4928949bb6..afb7e7e8637e 100644 --- a/source/isaaclab_physx/isaaclab_physx/benchmark/assets/runtime.py +++ b/source/isaaclab_physx/isaaclab_physx/benchmark/assets/runtime.py @@ -12,8 +12,11 @@ from types import ModuleType, SimpleNamespace from unittest.mock import MagicMock, patch +from isaaclab.actuators import ActuatorCollection from isaaclab.benchmark.asset_suites.types import AssetBenchmarkTargets +from isaaclab_physx.assets.articulation.actuator_control import PhysxActuatorControl + args = SimpleNamespace(no_shape_checks=False) @@ -112,6 +115,7 @@ def create_test_articulation( object.__setattr__(articulation, "_root_view", mock_view) object.__setattr__(articulation, "_device", device) object.__setattr__(articulation, "_check_shapes", not args.no_shape_checks) + object.__setattr__(articulation, "_sim_cfg", SimpleNamespace(use_newton_actuators=False)) # Create ArticulationData instance (SimulationManager already mocked at module level) data = ArticulationData(mock_view, device) @@ -141,11 +145,6 @@ def create_test_articulation( object.__setattr__(articulation, "_ALL_INDICES_WP", all_indices_wp) object.__setattr__(articulation, "_ALL_BODY_INDICES_WP", all_body_indices_wp) - # Initialize joint targets - object.__setattr__(articulation, "_joint_pos_target_sim", torch.zeros(num_instances, num_joints, device=device)) - object.__setattr__(articulation, "_joint_vel_target_sim", torch.zeros(num_instances, num_joints, device=device)) - object.__setattr__(articulation, "_joint_effort_target_sim", torch.zeros(num_instances, num_joints, device=device)) - # Cached .view() wrappers object.__setattr__(articulation, "_root_link_pose_w_f32", None) object.__setattr__(articulation, "_root_com_vel_w_f32", None) @@ -186,6 +185,10 @@ def create_test_articulation( articulation, "_cpu_body_inertia", wp.zeros((N, B, 9), dtype=wp.float32, device="cpu", pinned=True) ) + control = PhysxActuatorControl(articulation) + object.__setattr__(articulation, "actuators", ActuatorCollection({}, control)) + data.bind_actuator_collection(articulation.actuators) + return articulation, mock_view, None diff --git a/source/isaaclab_physx/test/assets/test_articulation.py b/source/isaaclab_physx/test/assets/test_articulation.py index a778dd4d290f..6263ce09eb84 100644 --- a/source/isaaclab_physx/test/assets/test_articulation.py +++ b/source/isaaclab_physx/test/assets/test_articulation.py @@ -40,7 +40,7 @@ import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils import isaaclab.utils.string as string_utils -from isaaclab.actuators import ActuatorBase, IdealPDActuatorCfg, ImplicitActuatorCfg +from isaaclab.actuators import IdealPDActuatorCfg, ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg, get_articulation_name_ordering from isaaclab.controllers import ( DifferentialIKController, @@ -70,10 +70,10 @@ def generate_articulation_cfg( articulation_type: str, stiffness: float | None = 10.0, damping: float | None = 2.0, - velocity_limit: float | None = None, - effort_limit: float | None = None, - velocity_limit_sim: float | None = None, - effort_limit_sim: float | None = None, + 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. @@ -85,13 +85,13 @@ def generate_articulation_cfg( Defaults to 10.0. damping: Damping value for the articulation's actuators. Only currently used for "humanoid". Defaults to 2.0. - velocity_limit: Velocity limit for the actuators. Only currently used for "single_joint_implicit" + actuator_velocity_limit: Velocity limit for the actuators. Only currently used for "single_joint_implicit" and "single_joint_explicit". - effort_limit: Effort limit for the actuators. Only currently used for "single_joint_implicit" - and "single_joint_explicit". - velocity_limit_sim: Velocity limit for the actuators (set into the simulation). + 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". - effort_limit_sim: Effort limit for the actuators (set into the simulation). + joint_effort_limit: Effort limit for the actuators (set into the simulation). Only currently used for "single_joint_implicit" and "single_joint_explicit". Returns: @@ -122,10 +122,9 @@ def generate_articulation_cfg( actuators={ "joint": ImplicitActuatorCfg( joint_names_expr=[".*"], - effort_limit_sim=effort_limit_sim, - velocity_limit_sim=velocity_limit_sim, - effort_limit=effort_limit, - velocity_limit=velocity_limit, + joint_effort_limit=joint_effort_limit, + joint_velocity_limit=joint_velocity_limit, + actuator_velocity_limit=actuator_velocity_limit, stiffness=2000.0, damping=100.0, ), @@ -146,10 +145,10 @@ def generate_articulation_cfg( actuators={ "joint": IdealPDActuatorCfg( joint_names_expr=[".*"], - effort_limit_sim=effort_limit_sim, - velocity_limit_sim=velocity_limit_sim, - effort_limit=effort_limit, - velocity_limit=velocity_limit, + 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, ), @@ -1762,36 +1761,24 @@ def test_setting_gains_from_cfg_dict(sim, num_articulations, device): torch.testing.assert_close(articulation.actuators["body"].damping, expected_damping) -@pytest.mark.parametrize("num_articulations", [1, 2]) @pytest.mark.parametrize("device", test_devices()) -@pytest.mark.parametrize("vel_limit_sim", [1e5, None]) -@pytest.mark.parametrize("vel_limit", [1e2, None]) -@pytest.mark.parametrize("add_ground_plane", [False]) -def test_setting_velocity_limit_implicit(sim, num_articulations, device, vel_limit_sim, vel_limit, add_ground_plane): - """Test setting of velocity limit for implicit actuators. - - This test verifies that: - 1. The solver clamp ``velocity_limit_sim`` is applied to the simulation; when unset, the - USD-authored value is kept - 2. The joint velocity limit ``velocity_limit`` is never pushed to the solver and keeps its - configured value; when unset, it falls back to the solver clamp - - Args: - sim: The simulation fixture - num_articulations: Number of articulations to test - device: The device to run the simulation on - vel_limit_sim: The velocity limit to set in simulation - vel_limit: The velocity limit to set in actuator +@pytest.mark.parametrize("joint_velocity_limit", [1e5, None]) +def test_setting_velocity_limit_writes_to_solver(sim, device, joint_velocity_limit): + """Test that the resolved joint velocity limit reaches the PhysX solver. + + The full limit-resolution matrix (config override vs. USD default, implicit and explicit + actuators, actuator-limit soft fallback) is covered on the Newton backend and at unit + level. This smoke test only verifies the PhysX write path: the configured limit (or the + USD-authored default when unset) lands in the native solver buffers and matches + ``data.joint_vel_limits``. """ - # create simulation articulation_cfg = generate_articulation_cfg( articulation_type="single_joint_implicit", - velocity_limit_sim=vel_limit_sim, - velocity_limit=vel_limit, + joint_velocity_limit=joint_velocity_limit, ) articulation, _ = generate_articulation( articulation_cfg=articulation_cfg, - num_articulations=num_articulations, + num_articulations=1, device=device, ) # Play sim @@ -1800,98 +1787,34 @@ def test_setting_velocity_limit_implicit(sim, num_articulations, device, vel_lim # read the values set into the simulation physx_vel_limit = wp.to_torch(articulation.root_view.get_dof_max_velocities()).to(device) # check data buffer - torch.testing.assert_close(articulation.data.joint_velocity_limits.torch, physx_vel_limit) - # check actuator has simulation velocity limit - torch.testing.assert_close(articulation.actuators["joint"].velocity_limit_sim, physx_vel_limit) - - # the solver clamp comes from velocity_limit_sim when set, otherwise the USD-authored value - if vel_limit_sim is None: - sim_limit = articulation_cfg.spawn.joint_drive_props.max_joint_velocity + torch.testing.assert_close(articulation.data.joint_vel_limits.torch, physx_vel_limit) + # the solver clamp comes from joint_velocity_limit when set, otherwise the USD-authored value + if joint_velocity_limit is None: + limit = articulation_cfg.spawn.joint_drive_props.max_joint_velocity else: - sim_limit = vel_limit_sim - expected_velocity_limit = torch.full_like(physx_vel_limit, sim_limit) + limit = joint_velocity_limit + expected_velocity_limit = torch.full_like(physx_vel_limit, limit) torch.testing.assert_close(physx_vel_limit, expected_velocity_limit) - # the joint velocity limit keeps its configured value and is not pushed to the solver; - # when unset it falls back to the solver clamp - joint_limit = vel_limit if vel_limit is not None else sim_limit - expected_joint_limit = torch.full_like(physx_vel_limit, joint_limit) - torch.testing.assert_close(articulation.actuators["joint"].velocity_limit, expected_joint_limit) - -@pytest.mark.parametrize("num_articulations", [1, 2]) @pytest.mark.parametrize("device", test_devices()) -@pytest.mark.parametrize("vel_limit_sim", [1e5, None]) -@pytest.mark.parametrize("vel_limit", [1e2, None]) -def test_setting_velocity_limit_explicit(sim, num_articulations, device, vel_limit_sim, vel_limit): - """Test setting of velocity limit for explicit actuators.""" - articulation_cfg = generate_articulation_cfg( - articulation_type="single_joint_explicit", - velocity_limit_sim=vel_limit_sim, - velocity_limit=vel_limit, - ) - articulation, _ = generate_articulation( - articulation_cfg=articulation_cfg, - num_articulations=num_articulations, - device=device, - ) - # Play sim - sim.reset() - - # collect limit init values - physx_vel_limit = wp.to_torch(articulation.root_view.get_dof_max_velocities()).to(device) - actuator_vel_limit = articulation.actuators["joint"].velocity_limit - actuator_vel_limit_sim = articulation.actuators["joint"].velocity_limit_sim - - # check data buffer for joint_velocity_limits_sim - torch.testing.assert_close(articulation.data.joint_velocity_limits.torch, physx_vel_limit) - # check actuator velocity_limit_sim is set to physx - torch.testing.assert_close(actuator_vel_limit_sim, physx_vel_limit) - - if vel_limit is not None: - expected_actuator_vel_limit = torch.full( - (articulation.num_instances, articulation.num_joints), - vel_limit, - device=articulation.device, - ) - # check actuator is set - torch.testing.assert_close(actuator_vel_limit, expected_actuator_vel_limit) - # check physx is not velocity_limit - assert not torch.allclose(actuator_vel_limit, physx_vel_limit) - else: - # check actuator velocity_limit is the same as the PhysX default - torch.testing.assert_close(actuator_vel_limit, physx_vel_limit) - - # simulation velocity limit is set to USD value unless user overrides - if vel_limit_sim is not None: - limit = vel_limit_sim - else: - limit = articulation_cfg.spawn.joint_drive_props.max_joint_velocity - # check physx is set to expected value - expected_vel_limit = torch.full_like(physx_vel_limit, limit) - torch.testing.assert_close(physx_vel_limit, expected_vel_limit) - - -@pytest.mark.parametrize("num_articulations", [1, 2]) -@pytest.mark.parametrize("device", test_devices()) -@pytest.mark.parametrize("effort_limit_sim", [1e5, None]) -@pytest.mark.parametrize("effort_limit", [1e2, 80.0, None]) -def test_setting_effort_limit_implicit(sim, num_articulations, device, effort_limit_sim, effort_limit): - """Test setting of effort limit for implicit actuators. - - This test verifies the effort limit resolution logic for actuator models implemented in :class:`ActuatorBase`: - - Case 1: If USD value == actuator config value: values match correctly - - Case 2: If USD value != actuator config value: actuator config value is used - - Case 3: If actuator config value is None: USD value is used as default +@pytest.mark.parametrize("joint_effort_limit", [1e5, None]) +def test_setting_effort_limit_writes_to_solver(sim, device, joint_effort_limit): + """Test that the resolved joint effort limit reaches the PhysX solver. + + The full limit-resolution matrix (config override vs. USD default, implicit and explicit + actuators, actuator-limit soft fallback) is covered on the Newton backend and at unit + level. This smoke test only verifies the PhysX write path: the configured limit (or the + USD-authored default when unset) lands in the native solver buffers and matches + ``data.joint_effort_limits``. """ articulation_cfg = generate_articulation_cfg( articulation_type="single_joint_implicit", - effort_limit_sim=effort_limit_sim, - effort_limit=effort_limit, + joint_effort_limit=joint_effort_limit, ) articulation, _ = generate_articulation( articulation_cfg=articulation_cfg, - num_articulations=num_articulations, + num_articulations=1, device=device, ) # Play sim @@ -1899,78 +1822,14 @@ def test_setting_effort_limit_implicit(sim, num_articulations, device, effort_li # obtain the physx effort limits physx_effort_limit = wp.to_torch(articulation.root_view.get_dof_max_forces()).to(device=device) - - # The solver clamp reaches the physics engine; the rated limit remains on the actuator. - torch.testing.assert_close(articulation.actuators["joint"].effort_limit_sim, physx_effort_limit) - - solver_limit = effort_limit_sim if effort_limit_sim is not None else effort_limit - if solver_limit is None: - solver_limit = articulation_cfg.spawn.joint_drive_props.max_force - rated_limit = effort_limit if effort_limit is not None else solver_limit - torch.testing.assert_close(physx_effort_limit, torch.full_like(physx_effort_limit, solver_limit)) - torch.testing.assert_close( - articulation.actuators["joint"].effort_limit, torch.full_like(physx_effort_limit, rated_limit) - ) - - -@pytest.mark.parametrize("num_articulations", [1, 2]) -@pytest.mark.parametrize("device", test_devices()) -@pytest.mark.parametrize("effort_limit_sim", [1e5, None]) -@pytest.mark.parametrize("effort_limit", [80.0, 1e2, None]) -def test_setting_effort_limit_explicit(sim, num_articulations, device, effort_limit_sim, effort_limit): - """Test setting of effort limit for explicit actuators. - - This test verifies the effort limit resolution logic for actuator models implemented in :class:`ActuatorBase`: - - Case 1: If USD value == actuator config value: values match correctly - - Case 2: If USD value != actuator config value: actuator config value is used - - Case 3: If actuator config value is None: USD value is used as default - - """ - - articulation_cfg = generate_articulation_cfg( - articulation_type="single_joint_explicit", - effort_limit_sim=effort_limit_sim, - effort_limit=effort_limit, - ) - articulation, _ = generate_articulation( - articulation_cfg=articulation_cfg, - num_articulations=num_articulations, - device=device, - ) - # Play sim - sim.reset() - - # usd default effort limit is set to 80 - usd_default_effort_limit = 80.0 - - # collect limit init values - physx_effort_limit = wp.to_torch(articulation.root_view.get_dof_max_forces()).to(device) - actuator_effort_limit = articulation.actuators["joint"].effort_limit - actuator_effort_limit_sim = articulation.actuators["joint"].effort_limit_sim - - # check actuator effort_limit_sim is set to physx - torch.testing.assert_close(actuator_effort_limit_sim, physx_effort_limit) - - if effort_limit is not None: - expected_actuator_effort_limit = torch.full_like(actuator_effort_limit, effort_limit) - # check actuator is set - torch.testing.assert_close(actuator_effort_limit, expected_actuator_effort_limit) - - # check physx effort limit does not match the one explicit actuator has - assert not (torch.allclose(actuator_effort_limit, physx_effort_limit)) - else: - # When effort_limit is None, actuator should use USD default values - expected_actuator_effort_limit = torch.full_like(physx_effort_limit, usd_default_effort_limit) - torch.testing.assert_close(actuator_effort_limit, expected_actuator_effort_limit) - - # when using explicit actuators, the limits are set to high unless user overrides - if effort_limit_sim is not None: - limit = effort_limit_sim + # check data buffer + torch.testing.assert_close(articulation.data.joint_effort_limits.torch, physx_effort_limit) + # the solver keeps the USD-authored limit unless the user overrides it explicitly + if joint_effort_limit is None: + limit = articulation_cfg.spawn.joint_drive_props.max_force else: - limit = ActuatorBase._DEFAULT_MAX_EFFORT_SIM # type: ignore - # check physx internal value matches the expected sim value + limit = joint_effort_limit expected_effort_limit = torch.full_like(physx_effort_limit, limit) - torch.testing.assert_close(actuator_effort_limit_sim, expected_effort_limit) torch.testing.assert_close(physx_effort_limit, expected_effort_limit) diff --git a/source/isaaclab_physx/test/assets/test_newton_actuators_physx.py b/source/isaaclab_physx/test/assets/test_newton_actuators_physx.py index abfedaad6f99..06e8e6f9e6f6 100644 --- a/source/isaaclab_physx/test/assets/test_newton_actuators_physx.py +++ b/source/isaaclab_physx/test/assets/test_newton_actuators_physx.py @@ -18,22 +18,40 @@ simulation_app = AppLauncher(headless=True).app -import json +import functools import os -import tempfile import unittest +from types import SimpleNamespace +import pytest import torch import warp as wp from isaaclab_physx.assets import Articulation +from isaaclab_physx.assets.articulation.actuator_control import PhysxActuatorControl from isaaclab_physx.physics import PhysxCfg import isaaclab.sim as sim_utils -from isaaclab.actuators import DCMotorCfg, DelayedPDActuatorCfg, IdealPDActuatorCfg, ImplicitActuatorCfg +from isaaclab.actuators import IdealPDActuatorCfg +from isaaclab.actuators.newton import read_group_parameter from isaaclab.sim import SimulationCfg, build_simulation_context +from isaaclab.test.utils.actuator_equivalence import ( + CARTPOLE_EXPLICIT_ACTUATORS, + DC_MOTOR_ACTUATORS, + DELAYED_PD_ACTUATORS, + IDEAL_PD_ACTUATORS, + IMPLICIT_ONLY_ACTUATORS, + MIXED_WITH_IMPLICIT_ACTUATORS, + ActuatorStateResetBase, + EquivalenceAssertionsMixin, + MockEnv, + build_dr_term, + make_dummy_lstm_checkpoint, + make_dummy_mlp_checkpoint, +) from isaaclab.test.utils.articulation_ordering import assert_articulation_ordering_trace_matches from isaaclab_assets import ANYMAL_C_CFG +from isaaclab_assets.robots.spot import joint_parameter_lookup as SPOT_KNEE_LOOKUP # --------------------------------------------------------------------------- # Constants @@ -59,79 +77,42 @@ ) -# --------------------------------------------------------------------------- -# Actuator configurations under test -# --------------------------------------------------------------------------- +def test_prepare_native_actuators_does_not_zero_solver_gains(monkeypatch): + """Leave solver gains untouched until collection construction resolves actuator defaults.""" + from isaaclab_physx.assets.articulation import actuator_control + + from isaaclab.actuators.newton import NewtonActuatorAdapter, PhysxActuatorWrapper + + joint_buffer = SimpleNamespace(warp=wp.zeros((1, 1), dtype=wp.float32, device="cpu")) + collection = SimpleNamespace( + target_command=SimpleNamespace(position=joint_buffer, velocity=joint_buffer, effort=joint_buffer) + ) + gain_writes = [] + articulation = SimpleNamespace( + _sim_cfg=SimpleNamespace(use_newton_actuators=True), + cfg=SimpleNamespace(prim_path="/World/Robot"), + joint_names=["joint"], + num_instances=1, + num_joints=1, + device="cpu", + _data=SimpleNamespace(joint_pos=joint_buffer, joint_vel=joint_buffer), + write_joint_stiffness_to_sim_index=lambda **_: gain_writes.append("stiffness"), + write_joint_damping_to_sim_index=lambda **_: gain_writes.append("damping"), + ) + wrapper = SimpleNamespace() + adapter = SimpleNamespace(joint_indices=wp.array([0], dtype=wp.int32), finalize=lambda _: None) + monkeypatch.setattr(actuator_control, "find_first_matching_prim", lambda _: None) + monkeypatch.setattr(PhysxActuatorWrapper, "create", lambda **_: wrapper) + monkeypatch.setattr(NewtonActuatorAdapter, "from_usd", lambda **_: adapter) + + native_groups = PhysxActuatorControl(articulation).prepare_native_actuators( + collection, + {"explicit": IdealPDActuatorCfg(joint_names_expr=["joint"], stiffness=None, damping=None)}, + ) + + assert native_groups == {"explicit"} + assert gain_writes == [] -IDEAL_PD_ACTUATORS = { - "legs": IdealPDActuatorCfg( - joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], - stiffness=40.0, - damping=5.0, - effort_limit=80.0, - ), -} - -DC_MOTOR_ACTUATORS = { - "legs": DCMotorCfg( - joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], - saturation_effort=120.0, - effort_limit=80.0, - velocity_limit=7.5, - stiffness={".*": 40.0}, - damping={".*": 5.0}, - ), -} - -MIXED_ACTUATORS = { - "hips": IdealPDActuatorCfg( - joint_names_expr=[".*HAA"], - stiffness=40.0, - damping=5.0, - effort_limit=80.0, - ), - "knees": DCMotorCfg( - joint_names_expr=[".*HFE", ".*KFE"], - saturation_effort=120.0, - effort_limit=80.0, - velocity_limit=7.5, - stiffness={".*": 40.0}, - damping={".*": 5.0}, - ), -} - -DELAYED_PD_ACTUATORS = { - "legs": DelayedPDActuatorCfg( - joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], - stiffness=40.0, - damping=5.0, - effort_limit=80.0, - min_delay=2, - max_delay=4, - ), -} - -MIXED_WITH_IMPLICIT_ACTUATORS = { - "hips": ImplicitActuatorCfg( - joint_names_expr=[".*HAA"], - stiffness=40.0, - damping=5.0, - ), - "thighs": IdealPDActuatorCfg( - joint_names_expr=[".*HFE"], - stiffness=40.0, - damping=5.0, - effort_limit=80.0, - ), - "knees": DCMotorCfg( - joint_names_expr=[".*KFE"], - saturation_effort=120.0, - effort_limit=80.0, - velocity_limit=7.5, - stiffness=40.0, - damping=5.0, - ), -} # --------------------------------------------------------------------------- # Simulation runner @@ -146,6 +127,7 @@ def _run_simulation( feedforward: float | None = None, joint_ordering: tuple[str, ...] | None = None, permutation_sensitive_commands: bool = False, + capture_first_compute: bool = False, ) -> dict: """Run ANYmal-C on PhysX and return recorded trajectories + telemetry. @@ -159,6 +141,7 @@ def _run_simulation( joint_ordering: Optional explicit public joint-name order. permutation_sensitive_commands: Whether to command distinct position, velocity, and effort values by physical joint name. + capture_first_compute: Whether to invoke the first actuator computation inside an outer CUDA capture. Returns: Recorded joint-name metadata, commands, public trajectories and torque telemetry, and adapter effort traces. @@ -218,19 +201,22 @@ def _run_simulation( articulation.set_joint_effort_target_index(target=effort_target) recorded_pos, recorded_vel = [], [] - recorded_computed, recorded_applied = [], [] - recorded_adapter_computed, recorded_adapter_applied = [], [] + recorded_computed_effort, recorded_applied_effort = [], [] + recorded_adapter_applied = [] + if capture_first_compute: + with wp.ScopedCapture(device=articulation.device, force_module_load=True): + articulation.actuators.compute(DT) for _ in range(num_steps): articulation.write_data_to_sim() sim.step() articulation.update(DT) recorded_pos.append(wp.to_torch(articulation.data.joint_pos).clone()) recorded_vel.append(wp.to_torch(articulation.data.joint_vel).clone()) - recorded_computed.append(wp.to_torch(articulation.data.computed_torque).clone()) - recorded_applied.append(wp.to_torch(articulation.data.applied_torque).clone()) + recorded_computed_effort.append(articulation.actuators.computed_effort.torch.clone()) + recorded_applied_effort.append(articulation.actuators.applied_effort.torch.clone()) if use_newton_actuators: - recorded_adapter_computed.append(wp.to_torch(articulation.data._sim_bind_joint_computed_effort).clone()) recorded_adapter_applied.append(wp.to_torch(articulation._physx_actuator_wrapper.joint_f_2d).clone()) + native_actuator_graph_count = len(getattr(articulation._actuator_control, "_native_actuator_graphs", ()) or ()) return { "joint_names": joint_names, @@ -239,16 +225,59 @@ def _run_simulation( "adapter_joint_names": joint_names, "joint_pos": recorded_pos, "joint_vel": recorded_vel, - "computed_torque": recorded_computed, - "applied_torque": recorded_applied, - "adapter_computed_effort": recorded_adapter_computed, + "computed_effort": recorded_computed_effort, + "applied_effort": recorded_applied_effort, "adapter_applied_effort": recorded_adapter_applied, "target_pos": target_pos.clone(), "target_vel": target_vel.clone(), "effort_target": None if effort_target is None else effort_target.clone(), + "native_actuator_graph_count": native_actuator_graph_count, } +def test_graphable_newton_actuators_capture_ping_pong_graphs() -> None: + result = _run_simulation(DELAYED_PD_ACTUATORS, use_newton_actuators=True, num_steps=2) + + assert result["native_actuator_graph_count"] == 2 + + +def test_newton_actuator_graph_capture_failure_falls_back_to_eager(monkeypatch: pytest.MonkeyPatch) -> None: + class FailingCapture: + def __init__(self, *args, **kwargs): + pass + + def __enter__(self): + raise RuntimeError("capture unavailable") + + def __exit__(self, exc_type, exc_value, traceback): + return False + + monkeypatch.setattr(wp, "ScopedCapture", FailingCapture) + + result = _run_simulation( + DC_MOTOR_ACTUATORS, + use_newton_actuators=True, + num_steps=2, + feedforward=1.0, + ) + + assert result["native_actuator_graph_count"] == 0 + assert len(result["joint_pos"]) == 2 + assert all(torch.isfinite(joint_pos).all() for joint_pos in result["joint_pos"]) + assert all(torch.any(effort != 0.0) for effort in result["applied_effort"]) + assert all(torch.any(effort != 0.0) for effort in result["adapter_applied_effort"]) + + +def test_stateful_newton_actuators_reject_outer_cuda_capture() -> None: + with pytest.raises(RuntimeError, match="stateful Newton actuators cannot run inside an outer CUDA graph capture"): + _run_simulation( + DELAYED_PD_ACTUATORS, + use_newton_actuators=True, + num_steps=0, + capture_first_compute=True, + ) + + def test_newton_actuator_rollout_matches_reversed_joint_ordering() -> None: """Match PhysX Newton-actuator traces under reversed public joint ordering.""" identity_result = _run_simulation( @@ -270,7 +299,7 @@ def test_newton_actuator_rollout_matches_reversed_joint_ordering() -> None: def _assert_newton_actuator_uses_current_joint_state( joint_ordering: tuple[str, ...] | None, *, num_steps: int = NUM_STEPS ) -> None: - """Check that ``applied_torque`` always matches the IdealPD formula on *this* step's true state. + """Check that ``applied_effort`` always matches the IdealPD formula on *this* step's true state. Ground truth is read every step via ``root_view.get_dof_positions()``/``get_dof_velocities()`` -- the raw PhysX view, bypassing :class:`ArticulationData`'s cached ``joint_pos``/``joint_vel`` shadow @@ -286,7 +315,7 @@ def _assert_newton_actuator_uses_current_joint_state( joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], stiffness=kp, damping=kd, - effort_limit=effort_limit, + actuator_effort_limit=effort_limit, ), } sim_cfg = SimulationCfg(dt=DT, physics=PhysxCfg(), use_newton_actuators=True) @@ -334,7 +363,7 @@ def to_user_order(raw_backend: wp.array) -> torch.Tensor: true_vel = to_user_order(articulation.root_view.get_dof_velocities()).clone() articulation.write_data_to_sim() - applied = wp.to_torch(articulation.data.applied_torque).clone() + applied = articulation.actuators.applied_effort.torch.clone() expected = torch.clamp(kp * (target_pos - true_pos) - kd * true_vel, -effort_limit, effort_limit) torch.testing.assert_close( @@ -343,7 +372,7 @@ def to_user_order(raw_backend: wp.array) -> torch.Tensor: atol=1e-3, rtol=1e-3, msg=( - f"applied_torque at step {step} does not match the IdealPD formula evaluated on this" + f"applied_effort at step {step} does not match the IdealPD formula evaluated on this" " step's true PhysX joint state -- the Newton actuator likely used a stale" " joint_pos/joint_vel shadow" ), @@ -354,21 +383,15 @@ def to_user_order(raw_backend: wp.array) -> torch.Tensor: def test_newton_actuator_identity_ordering_uses_current_joint_state() -> None: - """Sanity check: with identity joint ordering, ``applied_torque`` always reflects this step's state.""" + """Sanity check: with identity joint ordering, ``applied_effort`` always reflects this step's state.""" _assert_newton_actuator_uses_current_joint_state(None) def test_newton_actuator_reversed_ordering_uses_current_joint_state() -> None: - """Regression test: a non-identity ordering must not lag PhysX's true joint state by one step. - - ``_apply_actuator_model_newton`` binds ``w.joint_q``/``w.joint_qd`` once, at actuator setup, to - ``data.joint_pos``/``data.joint_vel``. With identity joint ordering those bindings alias PhysX-owned - memory directly and are always current. With non-identity ordering they alias an owned shadow buffer - that is only refreshed when the ``joint_pos``/``joint_vel`` *public* getters run -- which - :meth:`_apply_actuator_model_newton` itself only triggers *after* stepping the adapter (for torque - telemetry), i.e. one step too late for the adapter to see it. Explicit Newton PD actuators then - silently compute torques from one-physics-step-stale joint state whenever nothing else in that step - happens to read ``data.joint_pos``/``data.joint_vel`` first. + """Regression test: non-identity joint ordering must preserve current-state torque evaluation. + + The adapter must resolve joint state in articulation order before evaluating the actuator model, + regardless of the backend view ordering. """ reversed_joint_names = tuple(reversed(_ANYMAL_C_PHYSX_JOINT_NAMES)) _assert_newton_actuator_uses_current_joint_state(reversed_joint_names) @@ -379,23 +402,18 @@ def test_newton_actuator_reversed_ordering_uses_current_joint_state() -> None: # --------------------------------------------------------------------------- -class _EquivalenceTestBase(unittest.TestCase): +class _EquivalenceTestBase(EquivalenceAssertionsMixin, unittest.TestCase): """Base for Lab-vs-Newton equivalence tests on the PhysX backend. Subclasses set ``actuators`` to the config under test. ``setUpClass`` runs the simulation with both ``use_newton_actuators=False`` (Lab path) and ``True`` (Newton via PhysxActuatorWrapper) and stores the results. + The ``test_*_match`` oracles come from :class:`EquivalenceAssertionsMixin`. """ __test__ = False actuators: dict = {} feedforward: float | None = None - pos_atol: float = 2e-3 - pos_rtol: float = 1e-3 - vel_atol: float = 1e-2 - vel_rtol: float = 1e-2 - torque_atol: float = 1e-3 - torque_rtol: float = 1e-3 @classmethod def setUpClass(cls): @@ -410,50 +428,6 @@ def setUpClass(cls): feedforward=cls.feedforward, ) - def test_joint_positions_match(self): - for step_i, (lab, newton) in enumerate(zip(self.lab_result["joint_pos"], self.newton_result["joint_pos"])): - torch.testing.assert_close( - lab, - newton, - atol=self.pos_atol, - rtol=self.pos_rtol, - msg=f"Joint positions diverged at step {step_i}", - ) - - def test_joint_velocities_match(self): - for step_i, (lab, newton) in enumerate(zip(self.lab_result["joint_vel"], self.newton_result["joint_vel"])): - torch.testing.assert_close( - lab, - newton, - atol=self.vel_atol, - rtol=self.vel_rtol, - msg=f"Joint velocities diverged at step {step_i}", - ) - - def test_applied_torque_match(self): - for step_i, (lab, newton) in enumerate( - zip(self.lab_result["applied_torque"], self.newton_result["applied_torque"]) - ): - torch.testing.assert_close( - lab, - newton, - atol=self.torque_atol, - rtol=self.torque_rtol, - msg=f"applied_torque diverged at step {step_i}", - ) - - def test_computed_torque_match(self): - for step_i, (lab, newton) in enumerate( - zip(self.lab_result["computed_torque"], self.newton_result["computed_torque"]) - ): - torch.testing.assert_close( - lab, - newton, - atol=self.torque_atol, - rtol=self.torque_rtol, - msg=f"computed_torque diverged at step {step_i}", - ) - # --------------------------------------------------------------------------- # Equivalence tests with different actuator types @@ -474,13 +448,6 @@ class TestDCMotorEquivalence(_EquivalenceTestBase): actuators = DC_MOTOR_ACTUATORS -class TestMixedActuatorEquivalence(_EquivalenceTestBase): - """Mixed actuators (IdealPD on HAA, DCMotor on HFE/KFE): Lab vs Newton (PhysX).""" - - __test__ = True - actuators = MIXED_ACTUATORS - - class TestDelayedPDEquivalence(_EquivalenceTestBase): """DelayedPDActuator on all 12 joints: Lab vs Newton (PhysX). @@ -504,24 +471,9 @@ class TestMixedWithImplicitEquivalence(_EquivalenceTestBase): # --------------------------------------------------------------------------- -# Implicit-only fast-path: enable Newton-actuator branch on PhysX with no explicit groups +# Implicit + non-zero feedforward effort target on PhysX # --------------------------------------------------------------------------- -IMPLICIT_ONLY_ACTUATORS = { - "legs": ImplicitActuatorCfg( - joint_names_expr=[".*HAA", ".*HFE", ".*KFE"], - stiffness=40.0, - damping=5.0, - ), -} - - -class TestImplicitOnlyEquivalencePhysx(_EquivalenceTestBase): - """All-implicit articulation on PhysX with ``use_newton_actuators=True``: Lab vs fast-path.""" - - __test__ = True - actuators = IMPLICIT_ONLY_ACTUATORS - class TestImplicitWithFeedforwardEquivalencePhysx(_EquivalenceTestBase): """Implicit-only actuators with a non-zero feedforward effort target on PhysX.""" @@ -536,16 +488,6 @@ class TestImplicitWithFeedforwardEquivalencePhysx(_EquivalenceTestBase): # --------------------------------------------------------------------------- -CARTPOLE_EXPLICIT_ACTUATORS = { - "all_joints": IdealPDActuatorCfg( - joint_names_expr=["slider_to_cart", "cart_to_pole"], - stiffness=10.0, - damping=1.0, - effort_limit=100.0, - ), -} - - def _run_anymal_and_cartpole(use_newton_actuators: bool, *, num_steps: int = NUM_STEPS) -> dict: """Spawn ANYmal-C + Cartpole per env on PhysX (different DOF counts, base types).""" from isaaclab_assets import CARTPOLE_CFG # noqa: PLC0415 @@ -640,80 +582,66 @@ def test_cartpole_matches_lab(self): # --------------------------------------------------------------------------- -class _MockScene: - """Minimal stand-in for ``InteractiveScene`` accepted by ``ManagerTermBase``.""" - - def __init__(self, assets: dict, num_envs: int): - self._assets = assets - self.num_envs = num_envs - - def __getitem__(self, name: str): - return self._assets[name] - - -class _MockEnv: - """Minimal stand-in for ``ManagerBasedEnv`` for invoking DR terms. - - ``randomize_actuator_gains`` only reads ``env.scene[name]`` and - ``env.scene.num_envs`` (plus ``env.num_envs`` / ``env.device`` from the - ``ManagerTermBase`` properties). No simulator access is needed because - the DR term reaches the actuator adapter via ``self.the actuator adapter``. - """ - - def __init__(self, assets: dict, num_envs: int, device: str): - self.scene = _MockScene(assets, num_envs) - self.num_envs = num_envs - self.device = device - - -def _build_dr_term(env, asset_name, joint_ids=None): - from isaaclab.envs.mdp.events import randomize_actuator_gains # noqa: PLC0415 - from isaaclab.managers import EventTermCfg, SceneEntityCfg # noqa: PLC0415 - - asset_cfg = SceneEntityCfg(asset_name) - if joint_ids is not None: - asset_cfg.joint_ids = joint_ids - cfg = EventTermCfg( - func=randomize_actuator_gains, - params={ - "asset_cfg": asset_cfg, - "stiffness_distribution_params": (100.0, 100.0), - "damping_distribution_params": (5.0, 5.0), - "operation": "abs", - "distribution": "uniform", - }, - ) - return randomize_actuator_gains(cfg, env), asset_cfg - - class TestRandomizeActuatorGainsViaEventsPhysx(unittest.TestCase): """End-to-end DR test for the PhysX backend. Drives ``randomize_actuator_gains`` (events.py) and verifies the new - kp/kd values land in the per-articulation adapter's buffer at the - right cells — exercising the full path: events → - the actuator adapter → write_stiffness/damping → propagation - to controllers. - - With ``operation="abs"`` and ``distribution="uniform"`` over a - degenerate range ``(K, K)``, every randomized cell is set to exactly - ``K`` — so the assertions are deterministic. + kp/kd values reach the controllers of the articulation's Newton + actuators — exercising the full path: events → the actuator adapter → + write_stiffness/damping → propagation to controllers. The assertions + read the controllers back via the public + ``read_group_parameter``. + + The native-controller tests use degenerate ranges for exact expected values. + The implicit-storage regression instead seeds the generator and uses + non-degenerate ranges to verify one sampled payload reaches every storage. """ - @staticmethod - def _gather_param(adapter, num_envs, num_joints, attr, device): - """Reconstruct a ``(num_envs, num_joints)`` view of ``controller.`` across all actuators.""" - out = torch.zeros((num_envs, num_joints), device=device) - for act in adapter.actuators: - ctrl = act.controller - if not hasattr(ctrl, attr): - continue - flat_t = wp.to_torch(getattr(ctrl, attr)) - idx_np = act.indices.numpy() - envs = torch.from_numpy((idx_np // num_joints).astype("int64")).to(device) - locals_ = torch.from_numpy((idx_np % num_joints).astype("int64")).to(device) - out[envs, locals_] = flat_t - return out + def test_implicit_storage_reuses_randomized_payload(self): + """Keep actuator-owned and implicit-solver gains identical after randomization.""" + sim_cfg = SimulationCfg(dt=DT, physics=PhysxCfg(), use_newton_actuators=False) + with build_simulation_context( + device="cuda:0", + gravity_enabled=True, + add_ground_plane=True, + sim_cfg=sim_cfg, + ) as sim: + sim._app_control_on_stop_handle = None + for i in range(NUM_ENVS): + sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) + art_cfg = ANYMAL_C_CFG.replace( + actuators=IMPLICIT_ONLY_ACTUATORS, + prim_path="/World/Env_.*/Robot", + ) + anymal = Articulation(art_cfg) + sim.reset() + + actuator = anymal.actuators["legs"] + stiffness_before = actuator.stiffness.clone() + damping_before = actuator.damping.clone() + env = MockEnv({"robot": anymal}, NUM_ENVS, anymal.device) + term, asset_cfg = build_dr_term(env, "robot") + env_ids = torch.tensor([0], device=anymal.device, dtype=torch.long) + torch.manual_seed(12345) + + term( + env, + env_ids=env_ids, + asset_cfg=asset_cfg, + stiffness_distribution_params=(25.0, 75.0), + damping_distribution_params=(1.0, 9.0), + operation="abs", + distribution="uniform", + ) + + randomized_stiffness = actuator.stiffness[env_ids] + randomized_damping = actuator.damping[env_ids] + self.assertGreater(torch.unique(randomized_stiffness).numel(), 1) + self.assertGreater(torch.unique(randomized_damping).numel(), 1) + torch.testing.assert_close(randomized_stiffness, anymal.data.joint_stiffness.torch[env_ids]) + torch.testing.assert_close(randomized_damping, anymal.data.joint_damping.torch[env_ids]) + torch.testing.assert_close(actuator.stiffness[1:], stiffness_before[1:]) + torch.testing.assert_close(actuator.damping[1:], damping_before[1:]) def test_single_articulation(self): sim_cfg = SimulationCfg(dt=DT, physics=PhysxCfg(), use_newton_actuators=True) @@ -735,12 +663,13 @@ def test_single_articulation(self): adapter = anymal.newton_actuator_adapter self.assertIsNotNone(adapter, "PhysX per-articulation adapter should exist") + read = functools.partial(read_group_parameter, anymal.actuators) n = anymal.num_joints - kp_before = self._gather_param(adapter, NUM_ENVS, n, "kp", anymal.device).clone() - kd_before = self._gather_param(adapter, NUM_ENVS, n, "kd", anymal.device).clone() + kp_before = read("legs", "controller", "kp").clone() + kd_before = read("legs", "controller", "kd").clone() - env = _MockEnv({"robot": anymal}, NUM_ENVS, anymal.device) - term, asset_cfg = _build_dr_term(env, "robot") + env = MockEnv({"robot": anymal}, NUM_ENVS, anymal.device) + term, asset_cfg = build_dr_term(env, "robot") env_ids = torch.tensor([0], device=anymal.device, dtype=torch.long) term( @@ -753,13 +682,15 @@ def test_single_articulation(self): distribution="uniform", ) - kp_after = self._gather_param(adapter, NUM_ENVS, n, "kp", anymal.device) - kd_after = self._gather_param(adapter, NUM_ENVS, n, "kd", anymal.device) - torch.testing.assert_close(kp_after[0], torch.full((n,), 100.0, device=anymal.device)) - torch.testing.assert_close(kd_after[0], torch.full((n,), 5.0, device=anymal.device)) + # Named native-group reads project the controller values immediately. + torch.testing.assert_close( + read("legs", "controller", "kp")[0], torch.full((n,), 100.0, device=anymal.device) + ) + torch.testing.assert_close(read("legs", "controller", "kd")[0], torch.full((n,), 5.0, device=anymal.device)) + # Other envs untouched. for env_idx in range(1, NUM_ENVS): - torch.testing.assert_close(kp_after[env_idx], kp_before[env_idx]) - torch.testing.assert_close(kd_after[env_idx], kd_before[env_idx]) + torch.testing.assert_close(read("legs", "controller", "kp")[env_idx], kp_before[env_idx]) + torch.testing.assert_close(read("legs", "controller", "kd")[env_idx], kd_before[env_idx]) def test_two_articulations(self): from isaaclab_assets import CARTPOLE_CFG # noqa: PLC0415 @@ -792,15 +723,16 @@ def test_two_articulations(self): self.assertIsNotNone(cartpole_adapter) self.assertIsNot(anymal_adapter, cartpole_adapter) - n_anymal = anymal.num_joints + anymal_read = functools.partial(read_group_parameter, anymal.actuators) + cartpole_read = functools.partial(read_group_parameter, cartpole.actuators) n_cp = cartpole.num_joints - anymal_kp_before = self._gather_param(anymal_adapter, NUM_ENVS, n_anymal, "kp", anymal.device).clone() - anymal_kd_before = self._gather_param(anymal_adapter, NUM_ENVS, n_anymal, "kd", anymal.device).clone() - cp_kp_before = self._gather_param(cartpole_adapter, NUM_ENVS, n_cp, "kp", anymal.device).clone() - cp_kd_before = self._gather_param(cartpole_adapter, NUM_ENVS, n_cp, "kd", anymal.device).clone() + anymal_kp_before = anymal_read("legs", "controller", "kp").clone() + anymal_kd_before = anymal_read("legs", "controller", "kd").clone() + cp_kp_before = cartpole_read("all_joints", "controller", "kp").clone() + cp_kd_before = cartpole_read("all_joints", "controller", "kd").clone() - env = _MockEnv({"anymal": anymal, "cartpole": cartpole}, NUM_ENVS, anymal.device) - term, asset_cfg = _build_dr_term(env, "cartpole") + env = MockEnv({"anymal": anymal, "cartpole": cartpole}, NUM_ENVS, anymal.device) + term, asset_cfg = build_dr_term(env, "cartpole") env_ids = torch.tensor([0], device=anymal.device, dtype=torch.long) term( @@ -813,261 +745,47 @@ def test_two_articulations(self): distribution="uniform", ) - cp_kp_after = self._gather_param(cartpole_adapter, NUM_ENVS, n_cp, "kp", anymal.device) - cp_kd_after = self._gather_param(cartpole_adapter, NUM_ENVS, n_cp, "kd", anymal.device) + cp_kp_after = cartpole_read("all_joints", "controller", "kp") + cp_kd_after = cartpole_read("all_joints", "controller", "kd") torch.testing.assert_close(cp_kp_after[0], torch.full((n_cp,), 100.0, device=anymal.device)) torch.testing.assert_close(cp_kd_after[0], torch.full((n_cp,), 5.0, device=anymal.device)) + # Cartpole's other envs are untouched (env_ids=[0] only). for env_idx in range(1, NUM_ENVS): torch.testing.assert_close(cp_kp_after[env_idx], cp_kp_before[env_idx]) torch.testing.assert_close(cp_kd_after[env_idx], cp_kd_before[env_idx]) # ANYmal's controllers are fully untouched — DR was scoped to cartpole. - anymal_kp_after = self._gather_param(anymal_adapter, NUM_ENVS, n_anymal, "kp", anymal.device) - anymal_kd_after = self._gather_param(anymal_adapter, NUM_ENVS, n_anymal, "kd", anymal.device) - torch.testing.assert_close(anymal_kp_after, anymal_kp_before) - torch.testing.assert_close(anymal_kd_after, anymal_kd_before) + torch.testing.assert_close(anymal_read("legs", "controller", "kp"), anymal_kp_before) + torch.testing.assert_close(anymal_read("legs", "controller", "kd"), anymal_kd_before) # --------------------------------------------------------------------------- # Per-env reset: actuator state isolation # --------------------------------------------------------------------------- -RESET_WARMUP_STEPS = 3 +class TestActuatorStateReset(ActuatorStateResetBase, unittest.TestCase): + """Per-env actuator state reset isolation on the PhysX backend. -class TestActuatorStateReset(unittest.TestCase): - """Reset must clear the actuator state buffers for the requested envs only. - - Inspects ``adapter.actuators[i].state.delay_state.num_pushes`` directly: - - * After warmup, ``num_pushes > 0`` for every DOF (buffer was populated). - * After ``articulation.reset(env_ids=[0])``, the entries for env 0's DOFs - must be ``0`` and the entries for env 1's DOFs must remain ``> 0``. - - Done independently on Lab and Newton paths. PhysX-side adapter is - per-articulation, available via ``articulation.newton_actuator_adapter``. + The scenario and assertions live in :class:`ActuatorStateResetBase`; + this subclass provides the PhysX sim config and the per-articulation + adapter (``articulation.newton_actuator_adapter``). """ - RESET_ENV: int = 0 - UNCHANGED_ENV: int = 1 + def _make_sim_cfg(self, use_newton_actuators: bool) -> SimulationCfg: + return SimulationCfg(dt=DT, physics=PhysxCfg(), use_newton_actuators=use_newton_actuators) - def _build_and_warm(self, *, use_newton_actuators: bool): - sim_cfg = SimulationCfg( - dt=DT, - physics=PhysxCfg(), - use_newton_actuators=use_newton_actuators, - ) - ctx = build_simulation_context( - device="cuda:0", - gravity_enabled=True, - add_ground_plane=True, - sim_cfg=sim_cfg, - ) - sim = ctx.__enter__() - sim._app_control_on_stop_handle = None - for i in range(NUM_ENVS): - sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) - art_cfg = ANYMAL_C_CFG.replace( - actuators=DELAYED_PD_ACTUATORS, - prim_path="/World/Env_[^/]*/Robot", - ) - articulation = Articulation(art_cfg) - sim.reset() + def _make_articulation(self) -> Articulation: + return Articulation(ANYMAL_C_CFG.replace(actuators=DELAYED_PD_ACTUATORS, prim_path="/World/Env_.*/Robot")) - init_pos = wp.to_torch(articulation.data.joint_pos).clone() - target_pos = init_pos + TARGET_OFFSET - target_vel = torch.zeros_like(init_pos) - articulation.set_joint_position_target_index(target=target_pos) - articulation.set_joint_velocity_target_index(target=target_vel) - for _ in range(RESET_WARMUP_STEPS): - articulation.write_data_to_sim() - sim.step() - articulation.update(DT) - return ctx, sim, articulation - - def test_newton_state_reset_isolated_to_reset_env(self): - """Newton: ``num_pushes`` zeroes for env 0's DOFs only after reset of [0].""" - ctx, sim, articulation = self._build_and_warm(use_newton_actuators=True) - try: - adapter = articulation.newton_actuator_adapter - self.assertIsNotNone(adapter) - stateful_pairs = [ - (act, st) - for act, st in zip(adapter.actuators, adapter._states_a) - if st is not None and getattr(st, "delay_state", None) is not None - ] - self.assertGreater(len(stateful_pairs), 0, "expected at least one DelayedPD actuator with delay_state") - - for act, state in stateful_pairs: - pushes_before = state.delay_state.num_pushes.numpy() - self.assertTrue( - (pushes_before > 0).all(), - "expected non-zero num_pushes for all DOFs after warmup", - ) - - articulation.reset(env_ids=torch.tensor([self.RESET_ENV], device=articulation.device, dtype=torch.long)) - - # Map each entry of ``act.indices`` to its env via ``adapter.num_joints`` - # (PhysX adapter is per-articulation so this equals articulation.num_joints — - # using adapter.num_joints keeps the test symmetric with the Newton path). - for act, state in stateful_pairs: - pushes_after = state.delay_state.num_pushes.numpy() - indices_np = act.indices.numpy() - for i, global_dof in enumerate(indices_np): - env = int(global_dof) // adapter.num_joints - if env == self.RESET_ENV: - self.assertEqual( - int(pushes_after[i]), - 0, - f"DOF {i} (env {env}) should be reset to 0, got {pushes_after[i]}", - ) - else: - self.assertGreater( - int(pushes_after[i]), - 0, - f"DOF {i} (env {env}) was NOT in reset env_ids but num_pushes is 0", - ) - finally: - ctx.__exit__(None, None, None) - - def test_lab_state_reset_isolated_to_reset_env(self): - """Lab: DelayedPDActuator circular buffer zeroed for env 0 only.""" - ctx, sim, articulation = self._build_and_warm(use_newton_actuators=False) - try: - from isaaclab.actuators import DelayedPDActuator # noqa: PLC0415 - - delayed = [a for a in articulation.actuators.values() if isinstance(a, DelayedPDActuator)] - self.assertGreater(len(delayed), 0, "expected at least one Lab DelayedPDActuator") - actuator = delayed[0] - buf = actuator.positions_delay_buffer._circular_buffer._buffer - self.assertIsNotNone(buf, "delay buffer should be populated after warmup") - self.assertTrue( - (buf[:, self.UNCHANGED_ENV] != 0).any().item(), - "expected non-zero buffer entries for env 1 after warmup", - ) - - articulation.reset(env_ids=torch.tensor([self.RESET_ENV], device=articulation.device, dtype=torch.long)) - - self.assertTrue( - torch.all(buf[:, self.RESET_ENV] == 0).item(), - f"Lab: env {self.RESET_ENV} buffer not zeroed after reset.", - ) - self.assertTrue( - (buf[:, self.UNCHANGED_ENV] != 0).any().item(), - f"Lab: env {self.UNCHANGED_ENV} buffer was zeroed — reset leaked into an unselected env.", - ) - finally: - ctx.__exit__(None, None, None) + def _get_adapter(self, articulation): + return articulation.newton_actuator_adapter # --------------------------------------------------------------------------- -# RemotizedPD authoring: PD + delay + position-based clamping lookup table +# RemotizedPD equivalence: PD + delay + position-based clamping lookup table # --------------------------------------------------------------------------- -SPOT_KNEE_LOOKUP = [ - [-2.792900, -24.776718, 37.165077], - [-2.767442, -26.290108, 39.435162], - [-2.741984, -27.793369, 41.690054], - [-2.716526, -29.285997, 43.928996], - [-2.691068, -30.767536, 46.151304], - [-2.665610, -32.237423, 48.356134], - [-2.640152, -33.695168, 50.542751], - [-2.614694, -35.140221, 52.710331], - [-2.589236, -36.572052, 54.858078], - [-2.563778, -37.990086, 56.985128], - [-2.538320, -39.393730, 59.090595], - [-2.512862, -40.782406, 61.173609], - [-2.487404, -42.155487, 63.233231], - [-2.461946, -43.512371, 65.268557], - [-2.436488, -44.852371, 67.278557], - [-2.411030, -46.174873, 69.262310], - [-2.385572, -47.479156, 71.218735], - [-2.360114, -48.764549, 73.146824], - [-2.334656, -50.030334, 75.045502], - [-2.309198, -51.275761, 76.913641], - [-2.283740, -52.500103, 78.750154], - [-2.258282, -53.702587, 80.553881], - [-2.232824, -54.882442, 82.323664], - [-2.207366, -56.038860, 84.058290], - [-2.181908, -57.171028, 85.756542], - [-2.156450, -58.278133, 87.417200], - [-2.130992, -59.359314, 89.038971], - [-2.105534, -60.413738, 90.620607], - [-2.080076, -61.440529, 92.160793], - [-2.054618, -62.438812, 93.658218], - [-2.029160, -63.407692, 95.111538], - [-2.003702, -64.346268, 96.519402], - [-1.978244, -65.253670, 97.880505], - [-1.952786, -66.128944, 99.193417], - [-1.927328, -66.971176, 100.456764], - [-1.901870, -67.779457, 101.669186], - [-1.876412, -68.552864, 102.829296], - [-1.850954, -69.290451, 103.935677], - [-1.825496, -69.991325, 104.986988], - [-1.800038, -70.654541, 105.981812], - [-1.774580, -71.279190, 106.918785], - [-1.749122, -71.864319, 107.796478], - [-1.723664, -72.409088, 108.613632], - [-1.698206, -72.912567, 109.368851], - [-1.672748, -73.373871, 110.060806], - [-1.647290, -73.792130, 110.688194], - [-1.621832, -74.166512, 111.249767], - [-1.596374, -74.496147, 111.744221], - [-1.570916, -74.780251, 112.170376], - [-1.545458, -75.017998, 112.526997], - [-1.520000, -75.208656, 112.812984], - [-1.494542, -75.351448, 113.027172], - [-1.469084, -75.445686, 113.168530], - [-1.443626, -75.490677, 113.236015], - [-1.418168, -75.485771, 113.228657], - [-1.392710, -75.430344, 113.145515], - [-1.367252, -75.323830, 112.985744], - [-1.341794, -75.165688, 112.748531], - [-1.316336, -74.955406, 112.433109], - [-1.290878, -74.692551, 112.038826], - [-1.265420, -74.376694, 111.565041], - [-1.239962, -74.007477, 111.011215], - [-1.214504, -73.584579, 110.376869], - [-1.189046, -73.107742, 109.661613], - [-1.163588, -72.576752, 108.865128], - [-1.138130, -71.991455, 107.987183], - [-1.112672, -71.351707, 107.027561], - [-1.087214, -70.657486, 105.986229], - [-1.061756, -69.908813, 104.863220], - [-1.036298, -69.105721, 103.658581], - [-1.010840, -68.248337, 102.372505], - [-0.985382, -67.336861, 101.005291], - [-0.959924, -66.371513, 99.557270], - [-0.934466, -65.352615, 98.028923], - [-0.909008, -64.280533, 96.420799], - [-0.883550, -63.155693, 94.733540], - [-0.858092, -61.978588, 92.967882], - [-0.832634, -60.749775, 91.124662], - [-0.807176, -59.469845, 89.204767], - [-0.781718, -58.139503, 87.209255], - [-0.756260, -56.759487, 85.139231], - [-0.730802, -55.330616, 82.995924], - [-0.705344, -53.853729, 80.780594], - [-0.679886, -52.329796, 78.494694], - [-0.654428, -50.759762, 76.139643], - [-0.628970, -49.144699, 73.717049], - [-0.603512, -47.485737, 71.228605], - [-0.578054, -45.784004, 68.676006], - [-0.552596, -44.040764, 66.061146], - [-0.527138, -42.257267, 63.385900], - [-0.501680, -40.434883, 60.652325], - [-0.476222, -38.574947, 57.862421], - [-0.450764, -36.678982, 55.018473], - [-0.425306, -34.748432, 52.122648], - [-0.399848, -32.784836, 49.177254], - [-0.374390, -30.789810, 46.184715], - [-0.348932, -28.764952, 43.147428], - [-0.323474, -26.711969, 40.067954], - [-0.298016, -24.632576, 36.948864], - [-0.272558, -22.528547, 33.792821], - [-0.247100, -20.401667, 30.602500], -] - class TestRemotizedPDEquivalence(_EquivalenceTestBase): """RemotizedPD (PD + delay + position-based clamping): Lab vs Newton (PhysX). @@ -1087,13 +805,13 @@ def setUpClass(cls): joint_names_expr=[".*HAA", ".*HFE"], stiffness=40.0, damping=5.0, - effort_limit=80.0, + actuator_effort_limit=80.0, ), "knees": RemotizedPDActuatorCfg( joint_names_expr=[".*KFE"], stiffness=60.0, damping=1.5, - effort_limit=80.0, + actuator_effort_limit=80.0, max_delay=3, joint_parameter_lookup=SPOT_KNEE_LOOKUP, ), @@ -1101,111 +819,11 @@ def setUpClass(cls): super().setUpClass() -class TestRemotizedPDFunctional(unittest.TestCase): - """Verify RemotizedPDActuatorCfg runs correctly on PhysX with Newton actuators. - - Uses the Spot knee lookup table (102 entries) on ANYmal's KFE joints. - """ - - @classmethod - def setUpClass(cls): - from isaaclab.actuators.actuator_pd_cfg import RemotizedPDActuatorCfg # noqa: PLC0415 - - cls.result = _run_simulation( - { - "hips": IdealPDActuatorCfg( - joint_names_expr=[".*HAA", ".*HFE"], - stiffness=40.0, - damping=5.0, - effort_limit=80.0, - ), - "knees": RemotizedPDActuatorCfg( - joint_names_expr=[".*KFE"], - stiffness=60.0, - damping=1.5, - effort_limit=80.0, - max_delay=3, - joint_parameter_lookup=SPOT_KNEE_LOOKUP, - ), - }, - use_newton_actuators=True, - ) - - def test_positions_finite(self): - for step_i, pos in enumerate(self.result["joint_pos"]): - self.assertTrue( - torch.isfinite(pos).all(), - f"Non-finite positions at step {step_i}", - ) - - # --------------------------------------------------------------------------- # Neural network actuator authoring: MLP and LSTM # --------------------------------------------------------------------------- -def _make_dummy_mlp_checkpoint(device: str = "cpu") -> str: - """Create a minimal TorchScript MLP checkpoint with metadata.""" - torch.manual_seed(42) - net = ( - torch.nn.Sequential( - torch.nn.Linear(6, 8), - torch.nn.ELU(), - torch.nn.Linear(8, 1), - ) - .to(device) - .eval() - ) - scripted = torch.jit.script(net) - - with tempfile.NamedTemporaryFile(suffix=".pt", delete=False) as tmp: - tmp_path = tmp.name - extra = { - "metadata.json": json.dumps( - { - "model_type": "mlp", - "input_order": "pos_vel", - "input_idx": [0, 1, 2], - "pos_scale": 1.0, - "vel_scale": 0.5, - "torque_scale": 2.0, - } - ) - } - torch.jit.save(scripted, tmp_path, _extra_files=extra) - return tmp_path - - -class _DummyLSTM(torch.nn.Module): - """Minimal LSTM network for actuator testing.""" - - def __init__(self): - super().__init__() - self.lstm = torch.nn.LSTM(input_size=2, hidden_size=4, num_layers=1, batch_first=True) - self.fc = torch.nn.Linear(4, 1) - - def forward( - self, - x: torch.Tensor, - hc: tuple[torch.Tensor, torch.Tensor], - ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]: - out, hc_new = self.lstm(x, hc) - return self.fc(out[:, -1, :]), hc_new - - -def _make_dummy_lstm_checkpoint(device: str = "cpu") -> str: - """Create a minimal TorchScript LSTM checkpoint with metadata.""" - torch.manual_seed(42) - net = _DummyLSTM().to(device).eval() - scripted = torch.jit.script(net) - - with tempfile.NamedTemporaryFile(suffix=".pt", delete=False) as tmp: - tmp_path = tmp.name - extra = {"metadata.json": json.dumps({"model_type": "lstm"})} - torch.jit.save(scripted, tmp_path, _extra_files=extra) - return tmp_path - - class TestNeuralMLPFunctional(unittest.TestCase): """Verify ActuatorNetMLPCfg runs on PhysX with Newton actuators.""" @@ -1213,15 +831,15 @@ class TestNeuralMLPFunctional(unittest.TestCase): def setUpClass(cls): from isaaclab.actuators.actuator_net_cfg import ActuatorNetMLPCfg # noqa: PLC0415 - cls.mlp_path = _make_dummy_mlp_checkpoint() + cls.mlp_path = make_dummy_mlp_checkpoint() cls.result = _run_simulation( { "mlp_legs": ActuatorNetMLPCfg( joint_names_expr=[".*HAA"], network_file=cls.mlp_path, saturation_effort=120.0, - effort_limit=80.0, - velocity_limit=7.5, + actuator_effort_limit=80.0, + actuator_velocity_limit=7.5, pos_scale=-1.0, vel_scale=1.0, torque_scale=1.0, @@ -1232,7 +850,7 @@ def setUpClass(cls): joint_names_expr=[".*HFE", ".*KFE"], stiffness=40.0, damping=5.0, - effort_limit=80.0, + actuator_effort_limit=80.0, ), }, use_newton_actuators=True, @@ -1257,21 +875,21 @@ class TestNeuralLSTMFunctional(unittest.TestCase): def setUpClass(cls): from isaaclab.actuators.actuator_net_cfg import ActuatorNetLSTMCfg # noqa: PLC0415 - cls.lstm_path = _make_dummy_lstm_checkpoint() + cls.lstm_path = make_dummy_lstm_checkpoint() cls.result = _run_simulation( { "lstm_legs": ActuatorNetLSTMCfg( joint_names_expr=[".*HAA"], network_file=cls.lstm_path, saturation_effort=120.0, - effort_limit=80.0, - velocity_limit=7.5, + actuator_effort_limit=80.0, + actuator_velocity_limit=7.5, ), "pd_legs": IdealPDActuatorCfg( joint_names_expr=[".*HFE", ".*KFE"], stiffness=40.0, damping=5.0, - effort_limit=80.0, + actuator_effort_limit=80.0, ), }, use_newton_actuators=True, diff --git a/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py b/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py index 7b21de800dee..be7909f007bb 100644 --- a/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py +++ b/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py @@ -76,10 +76,10 @@ def _make_cartpole_articulation_cfg(pole_damping: float = 0.0) -> ArticulationCf ), actuators={ "cart_actuator": ImplicitActuatorCfg( - joint_names_expr=["slider_to_cart"], effort_limit_sim=400.0, stiffness=0.0, damping=10.0 + joint_names_expr=["slider_to_cart"], joint_effort_limit=400.0, stiffness=0.0, damping=10.0 ), "pole_actuator": ImplicitActuatorCfg( - joint_names_expr=["cart_to_pole"], effort_limit_sim=400.0, stiffness=0.0, damping=pole_damping + joint_names_expr=["cart_to_pole"], joint_effort_limit=400.0, stiffness=0.0, damping=pole_damping ), }, ) diff --git a/source/isaaclab_tasks/changelog.d/actuator-collection.minor.rst b/source/isaaclab_tasks/changelog.d/actuator-collection.minor.rst new file mode 100644 index 000000000000..49fd99a95ac4 --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/actuator-collection.minor.rst @@ -0,0 +1,8 @@ +Changed +^^^^^^^ + +* Changed the Unitree Go2 velocity tasks to execute their DC motor actuators + through the backend-native path by default + (:attr:`~isaaclab.sim.SimulationCfg.use_newton_actuators` is now ``True``). + Set ``env.sim.use_newton_actuators=false`` to restore Isaac Lab-side actuator + execution. diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env.py index 5d25326c2923..5fdd6949b196 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env.py @@ -134,7 +134,7 @@ def _get_rewards(self) -> torch.Tensor: # angular velocity x/y ang_vel_error = torch.sum(torch.square(self._robot.data.root_ang_vel_b.torch[:, :2]), dim=1) # joint torques - joint_torques = torch.sum(torch.square(self._robot.data.applied_torque.torch), dim=1) + joint_torques = torch.sum(torch.square(self._robot.actuators.applied_effort.torch), dim=1) # joint acceleration joint_accel = torch.sum(torch.square(self._robot.data.joint_acc.torch), dim=1) # action rate diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/observations.py index 58387ee7996c..d0c5b136eb37 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/observations.py @@ -41,10 +41,11 @@ def get_robot_body_joint_states(env: ManagerBasedRLEnv) -> torch.Tensor: """Return body joint states as a single tensor: [pos(29) | vel(29) | torque(29)].""" - robot_data = env.scene["robot"].data + robot = env.scene["robot"] + robot_data = robot.data joint_pos = robot_data.joint_pos.torch joint_vel = robot_data.joint_vel.torch - joint_torque = robot_data.applied_torque.torch + joint_torque = robot.actuators.applied_effort.torch device = joint_pos.device batch = joint_pos.shape[0] diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env_cfg.py index 40c528bdcbea..2483e5adc6fb 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env_cfg.py @@ -175,8 +175,8 @@ class AssemblyEnvCfg(DirectRLEnvCfg): damping=0.0, friction=0.0, armature=0.0, - effort_limit=87, - velocity_limit=124.6, + joint_effort_limit=87, + joint_velocity_limit=124.6, ), "panda_arm2": ImplicitActuatorCfg( joint_names_expr=["panda_joint[5-7]"], @@ -184,13 +184,13 @@ class AssemblyEnvCfg(DirectRLEnvCfg): damping=0.0, friction=0.0, armature=0.0, - effort_limit=12, - velocity_limit=149.5, + joint_effort_limit=12, + joint_velocity_limit=149.5, ), "panda_hand": ImplicitActuatorCfg( joint_names_expr=["panda_finger_joint[1-2]"], - effort_limit=40.0, - velocity_limit=0.04, + joint_effort_limit=40.0, + joint_velocity_limit=0.04, stiffness=7500.0, damping=173.0, friction=0.1, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env_cfg.py index 730d38685e3e..999859ec35bf 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env_cfg.py @@ -175,8 +175,8 @@ class DisassemblyEnvCfg(DirectRLEnvCfg): damping=0.0, friction=0.0, armature=0.0, - effort_limit=87, - velocity_limit=124.6, + joint_effort_limit=87, + joint_velocity_limit=124.6, ), "panda_arm2": ImplicitActuatorCfg( joint_names_expr=["panda_joint[5-7]"], @@ -184,13 +184,13 @@ class DisassemblyEnvCfg(DirectRLEnvCfg): damping=0.0, friction=0.0, armature=0.0, - effort_limit=12, - velocity_limit=149.5, + joint_effort_limit=12, + joint_velocity_limit=149.5, ), "panda_hand": ImplicitActuatorCfg( joint_names_expr=["panda_finger_joint[1-2]"], - effort_limit=40.0, - velocity_limit=0.04, + joint_effort_limit=40.0, + joint_velocity_limit=0.04, stiffness=7500.0, damping=173.0, friction=0.1, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/cabinet/config/openarm/cabinet_openarm_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/cabinet/config/openarm/cabinet_openarm_env_cfg.py index 8886d1a36442..34ce272c6545 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/cabinet/config/openarm/cabinet_openarm_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/cabinet/config/openarm/cabinet_openarm_env_cfg.py @@ -77,15 +77,15 @@ class CabinetSceneCfg(InteractiveSceneCfg): actuators={ "drawers": ImplicitActuatorCfg( joint_names_expr=["drawer_top_joint", "drawer_bottom_joint"], - effort_limit=87.0, - velocity_limit=100.0, + joint_effort_limit=87.0, + joint_velocity_limit=100.0, stiffness=10.0, damping=1.0, ), "doors": ImplicitActuatorCfg( joint_names_expr=["door_left_joint", "door_right_joint"], - effort_limit=87.0, - velocity_limit=100.0, + joint_effort_limit=87.0, + joint_velocity_limit=100.0, stiffness=10.0, damping=2.5, ), diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/gear_assembly/config/rizon_4s/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/gear_assembly/config/rizon_4s/joint_pos_env_cfg.py index 885e9b8b0d27..5777791ce7fc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/gear_assembly/config/rizon_4s/joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/gear_assembly/config/rizon_4s/joint_pos_env_cfg.py @@ -317,8 +317,8 @@ def __post_init__(self): # Grav gripper actuator configuration for gear manipulation self.scene.robot.actuators["gripper_drive"] = ImplicitActuatorCfg( joint_names_expr=["finger_joint"], - effort_limit_sim=2.0, - velocity_limit_sim=1.0, + joint_effort_limit=2.0, + joint_velocity_limit=1.0, stiffness=2e3, damping=1e1, friction=0.0, @@ -328,8 +328,8 @@ def __post_init__(self): # Passive/mimic joints in the gripper - set to zero stiffness/damping self.scene.robot.actuators["gripper_passive"] = ImplicitActuatorCfg( joint_names_expr=[".*_knuckle_joint"], - effort_limit_sim=1.0, - velocity_limit_sim=1.0, + joint_effort_limit=1.0, + joint_velocity_limit=1.0, stiffness=0.0, damping=0.0, friction=0.0, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/gear_assembly/config/ur_10e/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/gear_assembly/config/ur_10e/joint_pos_env_cfg.py index 626abb2bbccf..3cb9f4c13e31 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/gear_assembly/config/ur_10e/joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/gear_assembly/config/ur_10e/joint_pos_env_cfg.py @@ -356,8 +356,8 @@ def __post_init__(self): # 2F-140 gripper actuator configuration self.scene.robot.actuators["gripper_finger"] = ImplicitActuatorCfg( joint_names_expr=[".*_inner_finger_joint"], - effort_limit_sim=10.0, - velocity_limit_sim=10.0, + joint_effort_limit=10.0, + joint_velocity_limit=10.0, stiffness=10.0, damping=0.05, friction=0.0, @@ -445,8 +445,8 @@ def __post_init__(self): # 2F-85 gripper actuator configuration (higher effort limits than 2F-140) self.scene.robot.actuators["gripper_finger"] = ImplicitActuatorCfg( joint_names_expr=[".*_inner_finger_joint"], - effort_limit_sim=10.0, - velocity_limit_sim=10.0, + joint_effort_limit=10.0, + joint_velocity_limit=10.0, stiffness=10.0, damping=0.05, friction=0.0, @@ -454,8 +454,8 @@ def __post_init__(self): ) self.scene.robot.actuators["gripper_drive"] = ImplicitActuatorCfg( joint_names_expr=["finger_joint"], - effort_limit_sim=10.0, - velocity_limit_sim=1.0, + joint_effort_limit=10.0, + joint_velocity_limit=1.0, stiffness=40.0, damping=1.0, friction=0.0, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/rewards.py index 63a3263778b6..6cc3a6780427 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/rewards.py @@ -107,7 +107,7 @@ def joint_pd_command_l2( joint_ids = asset_cfg.joint_ids joint_pos = asset.data.joint_pos[:, joint_ids] joint_vel = asset.data.joint_vel[:, joint_ids] - joint_pos_target = asset.data.joint_pos_target[:, joint_ids] + joint_pos_target = asset.actuators.target_command.position.torch[:, joint_ids] pd_command = stiffness * (joint_pos_target - joint_pos) - damping * joint_vel return torch.sum(torch.square(pd_command), dim=1) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env_cfg.py index 9a82fafbcbe7..e3a44bbb5620 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/factory/factory_env_cfg.py @@ -177,8 +177,8 @@ class FactoryEnvCfg(DirectRLEnvCfg): damping=0.0, friction=0.0, armature=0.0, - effort_limit_sim=87, - velocity_limit_sim=124.6, + joint_effort_limit=87, + joint_velocity_limit=124.6, ), "panda_arm2": ImplicitActuatorCfg( joint_names_expr=["panda_joint[5-7]"], @@ -186,13 +186,13 @@ class FactoryEnvCfg(DirectRLEnvCfg): damping=0.0, friction=0.0, armature=0.0, - effort_limit_sim=12, - velocity_limit_sim=149.5, + joint_effort_limit=12, + joint_velocity_limit=149.5, ), "panda_hand": ImplicitActuatorCfg( joint_names_expr=["panda_finger_joint[1-2]"], - effort_limit_sim=40.0, - velocity_limit_sim=0.04, + joint_effort_limit=40.0, + joint_velocity_limit=0.04, stiffness=7500.0, damping=173.0, friction=0.1, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/humanoid_amp/humanoid_amp_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/humanoid_amp/humanoid_amp_env_cfg.py index 36ac3bd3637e..b38635542f44 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/humanoid_amp/humanoid_amp_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/humanoid_amp/humanoid_amp_env_cfg.py @@ -67,7 +67,7 @@ class HumanoidAmpEnvCfg(DirectRLEnvCfg): joint_names_expr=[".*"], stiffness=None, damping=None, - velocity_limit_sim={ + joint_velocity_limit={ ".*": 100.0, }, ), diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/mdp/observations.py index 4057bc6fe590..42fe9d9249e2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/mdp/observations.py @@ -21,7 +21,7 @@ def upper_body_last_action( ) -> torch.Tensor: """Extract the last action of the upper body.""" asset = env.scene[asset_cfg.name] - joint_pos_target = asset.data.joint_pos_target.torch + joint_pos_target = asset.actuators.target_command.position.torch # Use joint_names from asset_cfg to find indices joint_names = asset_cfg.joint_names if hasattr(asset_cfg, "joint_names") else None diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/config/so101/stack_ik_abs_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/config/so101/stack_ik_abs_env_cfg.py index 39f9f01f370a..6c176ab06162 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/config/so101/stack_ik_abs_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/config/so101/stack_ik_abs_env_cfg.py @@ -218,7 +218,7 @@ def __post_init__(self): # cube and ejecting it. The Franka panda_hand (effort 200 N, stiffness 2e3) is a *prismatic* # finger pair, so its gains do not transfer to this *revolute* jaw [N·m, rad]; instead we # behavior-match by capping the closing speed and grip torque so it closes gently and holds - # without penetrating. ``effort_limit_sim`` is the "strength" knob and ``velocity_limit_sim`` + # without penetrating. ``joint_effort_limit`` is the "strength" knob and ``joint_velocity_limit`` # the "speed" knob; stiffness/damping keep the asset defaults. Tune in-sim (lower effort if # it still pushes through; raise it if the cube drops). self.scene.robot = SO101_HIGH_PD_CFG.replace( @@ -228,8 +228,8 @@ def __post_init__(self): "arm": SO101_HIGH_PD_CFG.actuators["arm"], "gripper": ImplicitActuatorCfg( joint_names_expr=["gripper"], - effort_limit_sim=1.0, # was 10.0 -- cap grip torque so it can't push through - velocity_limit_sim=2.0, # was 10.0 -- close gently instead of snapping shut + joint_effort_limit=1.0, # was 10.0 -- cap grip torque so it can't push through + joint_velocity_limit=2.0, # was 10.0 -- close gently instead of snapping shut stiffness=17.8, damping=0.60, ), diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/velocity/config/spot/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/velocity/config/spot/mdp/rewards.py index 5cb88e0420aa..7bc5ffab830d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/velocity/config/spot/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/velocity/config/spot/mdp/rewards.py @@ -278,7 +278,7 @@ def joint_torques_penalty(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> """Penalize joint torques on the articulation.""" # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] - return torch.linalg.norm((asset.data.applied_torque.torch), dim=1) + return torch.linalg.norm(asset.actuators.applied_effort.torch, dim=1) def joint_velocity_penalty(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_env_cfg.py index d09aeec2de50..96490e8691b7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_env_cfg.py @@ -55,13 +55,13 @@ actuators={ "drawers": ImplicitActuatorCfg( joint_names_expr=["drawer_top_joint", "drawer_bottom_joint"], - effort_limit_sim=87.0, + joint_effort_limit=87.0, stiffness=10.0, damping=1.0, ), "doors": ImplicitActuatorCfg( joint_names_expr=["door_left_joint", "door_right_joint"], - effort_limit_sim=87.0, + joint_effort_limit=87.0, stiffness=10.0, damping=2.5, ), diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka/franka_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka/franka_env_cfg.py index cfeaf46464b8..ac65fc920b49 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka/franka_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka/franka_env_cfg.py @@ -25,12 +25,14 @@ FRANKA_PANDA_LIFT_CFG = FRANKA_PANDA_CFG.copy() FRANKA_PANDA_LIFT_CFG.spawn.usd_path = f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/franka_panda.usda" FRANKA_PANDA_LIFT_CFG.actuators = { - # inspired by libfranka's joint_impedance_control.cpp + # Inspired by libfranka's joint_impedance_control.cpp. ``actuator_velocity_limit`` + # remains the soft task-limit snapshot; ``joint_velocity_limit`` is the + # separate solver request. "panda_arm": ImplicitActuatorCfg( joint_names_expr=["panda_joint[1-7]"], - effort_limit_sim={"panda_joint[1-4]": 87.0, "panda_joint[5-7]": 12.0}, - velocity_limit={"panda_joint[1-4]": 2.175, "panda_joint[5-7]": 2.61}, - velocity_limit_sim={"panda_joint[1-4]": 20.0, "panda_joint[5-7]": 25.0}, + joint_effort_limit={"panda_joint[1-4]": 87.0, "panda_joint[5-7]": 12.0}, + actuator_velocity_limit={"panda_joint[1-4]": 2.175, "panda_joint[5-7]": 2.61}, + joint_velocity_limit={"panda_joint[1-4]": 20.0, "panda_joint[5-7]": 25.0}, stiffness={ "panda_joint[1-4]": 600.0, "panda_joint5": 250.0, @@ -51,18 +53,18 @@ ), "panda_hand": ImplicitActuatorCfg( joint_names_expr=["panda_finger_joint1"], - effort_limit_sim=70.0, - velocity_limit=0.2, - velocity_limit_sim=2.0, + joint_effort_limit=70.0, + actuator_velocity_limit=0.2, + joint_velocity_limit=2.0, stiffness=350.0, damping=175.0, armature=0.1, ), "panda_finger2_passive": ImplicitActuatorCfg( joint_names_expr=["panda_finger_joint2"], - effort_limit_sim=1.0, - velocity_limit=0.2, - velocity_limit_sim=2.0, + joint_effort_limit=1.0, + actuator_velocity_limit=0.2, + joint_velocity_limit=2.0, stiffness=0.0, damping=0.0, armature=0.1, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py index a20cd141a5ab..df3c65ae3423 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py @@ -184,6 +184,14 @@ class FrankaClothSceneCfg(_FrankaSoftSceneCfg): spawn=SUPPORT_SPAWN_CFG, ) + def __post_init__(self) -> None: + super().__post_init__() + + # increase franka gripper stiffness + self.robot.actuators["panda_hand"].joint_effort_limit = 500.0 + self.robot.actuators["panda_hand"].stiffness = 2000.0 + self.robot.actuators["panda_hand"].damping = 100.0 + @configclass class FrankaClothScenePresetCfg(PresetCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py index 97185161b8d1..6d9cb02ad7bf 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py @@ -250,8 +250,8 @@ def __post_init__(self) -> None: # inspired by libfranka's joint_impedance_control.cpp "panda_arm": ImplicitActuatorCfg( joint_names_expr=["panda_joint[1-7]"], - effort_limit_sim={"panda_joint[1-4]": 87.0, "panda_joint[5-7]": 12.0}, - velocity_limit_sim={"panda_joint[1-4]": 2.175, "panda_joint[5-7]": 2.61}, + joint_effort_limit={"panda_joint[1-4]": 87.0, "panda_joint[5-7]": 12.0}, + joint_velocity_limit={"panda_joint[1-4]": 2.175, "panda_joint[5-7]": 2.61}, stiffness={ "panda_joint[1-4]": 600.0, "panda_joint5": 250.0, @@ -272,24 +272,33 @@ def __post_init__(self) -> None: ), "panda_hand": ImplicitActuatorCfg( joint_names_expr=["panda_finger_joint1"], - effort_limit_sim=70.0, - velocity_limit=0.2, - velocity_limit_sim=2.0, + joint_effort_limit=70.0, + actuator_velocity_limit=0.2, + joint_velocity_limit=2.0, stiffness=350.0, damping=175.0, armature=0.1, ), "panda_finger2_passive": ImplicitActuatorCfg( joint_names_expr=["panda_finger_joint2"], - effort_limit_sim=1.0, - velocity_limit=0.2, - velocity_limit_sim=2.0, + joint_effort_limit=1.0, + actuator_velocity_limit=0.2, + joint_velocity_limit=2.0, stiffness=0.0, damping=0.0, armature=0.1, ), } + # disable gravity on the arm so the low-PD actuators do not need to fight gravity sag, + # which is the dominant source of steady-state IK tracking error. + self.robot.spawn.rigid_props.disable_gravity = True + + # increase franka gripper stiffness + self.robot.actuators["panda_hand"].joint_effort_limit = 500.0 + self.robot.actuators["panda_hand"].stiffness = 1000.0 + self.robot.actuators["panda_hand"].damping = 100.0 + @configclass class _FrankaSoftCameraSceneCfg(_FrankaSoftSceneCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/terminations.py index c7c77bff2ebb..7745b21337bd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/terminations.py @@ -145,10 +145,9 @@ def cable_outside_bounds( def joint_vel_out_of_sim_limit( env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") ) -> torch.Tensor: - """Terminate when joint velocities exceed actuator simulator limits [m/s or rad/s, depending on joint type].""" + """Terminate when joint velocities exceed solver limits [m/s or rad/s, depending on joint type].""" asset: Articulation = env.scene[asset_cfg.name] joint_ids = asset_cfg.joint_ids if asset_cfg.joint_ids is not None else slice(None) - limits = torch.full_like(asset.data.joint_vel.torch, torch.inf) - for actuator in asset.actuators.values(): - limits[:, actuator.joint_indices] = actuator.velocity_limit_sim - return torch.any(torch.abs(asset.data.joint_vel.torch[:, joint_ids]) > limits[:, joint_ids], dim=1) + return torch.any( + torch.abs(asset.data.joint_vel.torch[:, joint_ids]) > asset.data.joint_vel_limits.torch[:, joint_ids], dim=1 + ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/rough_env_cfg.py index 6c9287836541..2d948fd524ff 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/rough_env_cfg.py @@ -20,6 +20,9 @@ class UnitreeGo2RoughEnvCfg(LocomotionVelocityRoughEnvCfg): def __post_init__(self): super().__post_init__() + # simulation + # execute the DC motor actuators through the backend-native path + self.sim.use_newton_actuators = True # scene self.scene.robot = UNITREE_GO2_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") self.scene.robot.actuators["base_legs"].armature = preset(default=0.0, newton_mjwarp=0.02) diff --git a/source/isaaclab_tasks/test/core/test_newton_actuator_manager_authoring.py b/source/isaaclab_tasks/test/core/test_newton_actuator_manager_authoring.py index 8819a49f8a6a..87504c62e799 100644 --- a/source/isaaclab_tasks/test/core/test_newton_actuator_manager_authoring.py +++ b/source/isaaclab_tasks/test/core/test_newton_actuator_manager_authoring.py @@ -7,8 +7,8 @@ The default G1 config uses ``ImplicitActuatorCfg`` for every group, which intentionally skips ``NewtonActuator`` USD authoring. To exercise the -authoring path we override the scene's robot actuators with explicit -``DCMotorCfg`` groups covering the same joint patterns. +authoring path we override the scene's robot actuators with a single +explicit ``DCMotorCfg`` group covering all joints. """ from isaaclab.app import AppLauncher @@ -52,43 +52,17 @@ def test_newton_actuators_present_for_g1_manager_env(self): env_cfg.rewards.feet_slide = None env_cfg.terminations.base_contact = None env_cfg.sim = SimulationCfg(physics=_NEWTON_CFG, use_newton_actuators=True) + # A single explicit group covering all joints is enough to exercise the + # NewtonActuator authoring clone path; the group count is not the point. env_cfg.scene.robot.actuators = { - "legs": DCMotorCfg( - joint_names_expr=[ - ".*_hip_yaw_joint", - ".*_hip_roll_joint", - ".*_hip_pitch_joint", - ".*_knee_joint", - "torso_joint", - ], + "all": DCMotorCfg( + joint_names_expr=[".*"], saturation_effort=300.0, - effort_limit=300.0, - velocity_limit=20.0, + actuator_effort_limit=300.0, + actuator_velocity_limit=20.0, stiffness=150.0, damping=5.0, ), - "feet": DCMotorCfg( - joint_names_expr=[".*_ankle_pitch_joint", ".*_ankle_roll_joint"], - saturation_effort=20.0, - effort_limit=20.0, - velocity_limit=20.0, - stiffness=20.0, - damping=2.0, - ), - "arms": DCMotorCfg( - joint_names_expr=[ - ".*_shoulder_pitch_joint", - ".*_shoulder_roll_joint", - ".*_shoulder_yaw_joint", - ".*_elbow_pitch_joint", - ".*_elbow_roll_joint", - ], - saturation_effort=300.0, - effort_limit=300.0, - velocity_limit=20.0, - stiffness=40.0, - damping=10.0, - ), } env = ManagerBasedRLEnv(cfg=env_cfg) try: