Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/reinforcement_learning/rsl_rl/play.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg, agent_cfg: RslRlBaseRun
# export policy to onnx/jit
export_model_dir = os.path.join(os.path.dirname(resume_path), "exported")
export_policy_as_jit(policy_nn, normalizer=normalizer, path=export_model_dir, filename="policy.pt")
export_policy_as_onnx(policy_nn, normalizer=normalizer, path=export_model_dir, filename="policy.onnx")
# export_policy_as_onnx(policy_nn, normalizer=normalizer, path=export_model_dir, filename="policy.onnx")

dt = env.unwrapped.step_dt

Expand Down
117 changes: 106 additions & 11 deletions source/isaaclab/isaaclab/envs/mdp/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,13 +481,7 @@ def randomize_rigid_body_collider_offsets(
raise NotImplementedError("Not implemented")


def randomize_physics_scene_gravity(
env: ManagerBasedEnv,
env_ids: torch.Tensor | None,
gravity_distribution_params: tuple[list[float], list[float]],
operation: Literal["add", "scale", "abs"],
distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform",
):
class randomize_physics_scene_gravity(ManagerTermBase):
"""Randomize gravity by adding, scaling, or setting random values.

This function allows randomizing gravity of the physics scene. The function samples random values from the
Expand All @@ -498,13 +492,114 @@ def randomize_physics_scene_gravity(
distribution for the x, y, and z components of the gravity vector. The function samples random values for each
component independently.

.. attention::
This function applied the same gravity for all the environments.
This function supports **per-environment gravity randomization** using Newton's per-world gravity array.
Each environment (world) can have a different gravity vector.

.. note::
Gravity is set per-world using Newton's per-world gravity array support.
The randomization is applied only to the specified ``env_ids``.

.. tip::
This function uses CPU tensors to assign gravity.
This function uses GPU tensors for efficient per-environment gravity updates.
"""
raise NotImplementedError("Not implemented")

def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv):
"""Initialize the term and cache expensive objects.

Args:
cfg: The configuration of the event term.
env: The environment instance.
"""
super().__init__(cfg, env)

# Validate and cache distribution function
distribution = cfg.params.get("distribution", "uniform")
if distribution == "uniform":
self._dist_fn = math_utils.sample_uniform
elif distribution == "log_uniform":
self._dist_fn = math_utils.sample_log_uniform
elif distribution == "gaussian":
self._dist_fn = math_utils.sample_gaussian
else:
raise NotImplementedError(
f"Unknown distribution: '{distribution}' for gravity randomization."
" Please use 'uniform', 'log_uniform', or 'gaussian'."
)

# Validate operation
operation = cfg.params["operation"]
if operation not in ("add", "scale", "abs"):
raise NotImplementedError(
f"Unknown operation: '{operation}' for gravity randomization."
" Please use 'add', 'scale', or 'abs'."
)

# Cache distribution params as tensors
gravity_distribution_params = cfg.params["gravity_distribution_params"]
self._dist_param_0 = torch.tensor(
gravity_distribution_params[0], device=env.device, dtype=torch.float32
)
self._dist_param_1 = torch.tensor(
gravity_distribution_params[1], device=env.device, dtype=torch.float32
)

def __call__(
self,
env: ManagerBasedEnv,
env_ids: torch.Tensor | None,
gravity_distribution_params: tuple[list[float], list[float]],
operation: Literal["add", "scale", "abs"],
distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform",
):
"""Randomize gravity for the specified environments.

Args:
env: The environment instance.
env_ids: The environment IDs to randomize. If None, all environments are randomized.
gravity_distribution_params: Distribution parameters (cached, param ignored at runtime).
operation: The operation to apply ('add', 'scale', or 'abs').
distribution: The distribution type (cached, param ignored at runtime).
"""
model = NewtonManager.get_model()
if model is None or model.gravity is None:
raise RuntimeError("Newton model is not initialized. Cannot randomize gravity.")

# Get torch view of model.gravity - modifications are in-place
gravity = wp.to_torch(model.gravity)

# Resolve env_ids
if env_ids is None:
env_ids = env.scene._ALL_INDICES

if len(env_ids) == 0:
return

num_to_randomize = len(env_ids)

# Sample random values using cached distribution function and params
self._dist_param_0[0] = gravity_distribution_params[0][0]
self._dist_param_1[0] = gravity_distribution_params[1][0]
self._dist_param_0[1] = gravity_distribution_params[0][1]
self._dist_param_1[1] = gravity_distribution_params[1][1]
self._dist_param_0[2] = gravity_distribution_params[0][2]
self._dist_param_1[2] = gravity_distribution_params[1][2]
random_values = self._dist_fn(
self._dist_param_0.unsqueeze(0).expand(num_to_randomize, -1),
self._dist_param_1.unsqueeze(0).expand(num_to_randomize, -1),
(num_to_randomize, 3),
device=env.device,
)

# Apply operation directly to model.gravity (in-place via torch view)
if operation == "abs":
gravity[env_ids] = random_values
elif operation == "add":
gravity[env_ids] += random_values
elif operation == "scale":
gravity[env_ids] *= random_values

# Notify solver that model properties changed (required for gravity to take effect)
NewtonManager.add_model_change(SolverNotifyFlags.MODEL_PROPERTIES)


class randomize_actuator_gains(ManagerTermBase):
Expand Down
1 change: 1 addition & 0 deletions source/isaaclab/isaaclab/managers/event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ def _prepare_terms(self):

# check if the term is a class
if inspect.isclass(term_cfg.func):
term_cfg.func = term_cfg.func(cfg=term_cfg, env=self._env)
self._mode_class_term_cfgs[term_cfg.mode].append(term_cfg)

# resolve the mode of the events
Expand Down
14 changes: 7 additions & 7 deletions source/isaaclab/isaaclab/renderer/newton_warp_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import torch

import warp as wp
from newton.sensors import TiledCameraSensor
from newton.sensors import SensorTiledCamera

from isaaclab.sim._impl.newton_manager import NewtonManager
from isaaclab.utils.math import convert_camera_frame_orientation_convention
Expand Down Expand Up @@ -49,7 +49,7 @@ def _convert_raw_rgb_tiled(
This kernel converts it to the tiled format (tiled_height, tiled_width, 4) of uint8.

Args:
raw_buffer: Input raw buffer from TiledCameraSensor, shape (num_worlds, num_cameras, width * height) of uint32
raw_buffer: Input raw buffer from SensorTiledCamera, shape (num_worlds, num_cameras, width * height) of uint32
output_buffer: Output buffer in tiled format (tiled_height, tiled_width, 4) of uint8
image_width: Width of each camera image
image_height: Height of each camera image
Expand Down Expand Up @@ -99,7 +99,7 @@ def _convert_raw_depth_tiled(
This kernel converts it to the tiled format (tiled_height, tiled_width, 1) of float32.

Args:
raw_buffer: Input raw buffer from TiledCameraSensor, shape (num_worlds, num_cameras, width * height) of float32
raw_buffer: Input raw buffer from SensorTiledCamera, shape (num_worlds, num_cameras, width * height) of float32
output_buffer: Output buffer in tiled format (tiled_height, tiled_width, 1) of float32
image_width: Width of each camera image
image_height: Height of each camera image
Expand Down Expand Up @@ -143,12 +143,12 @@ def initialize(self):
"""Initialize the renderer."""
self._model = NewtonManager.get_model()

self._tiled_camera_sensor = TiledCameraSensor(
self._tiled_camera_sensor = SensorTiledCamera(
model=self._model,
num_cameras=1, # TODO: currently only supports 1 camera per world
width=self._width,
height=self._height,
options=TiledCameraSensor.Options(colors_per_shape=True),
options=SensorTiledCamera.Options(colors_per_shape=True),
)

# Note: camera rays will be computed when we have access to TiledCamera
Expand Down Expand Up @@ -180,7 +180,7 @@ def set_camera_rays_from_intrinsics(self, intrinsic_matrices: torch.Tensor):
fov_radians_wp = wp.from_torch(fov_radians_all, dtype=wp.float32)

# Compute camera rays with per-camera FOVs (vectorized)
# TiledCameraSensor.compute_pinhole_camera_rays accepts array of FOVs
# SensorTiledCamera.compute_pinhole_camera_rays accepts array of FOVs
self._camera_rays = self._tiled_camera_sensor.compute_pinhole_camera_rays(fov_radians_wp)

def _initialize_output(self):
Expand Down Expand Up @@ -238,7 +238,7 @@ def render(
device=camera_positions_wp.device,
)

# Render using TiledCameraSensor
# Render using SensorTiledCamera
self._tiled_camera_sensor.render(
state=NewtonManager.get_state_0(), # Use current physics state
camera_transforms=camera_transforms,
Expand Down
11 changes: 0 additions & 11 deletions source/isaaclab/isaaclab/sensors/camera/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import json
import numpy as np
import re
import torch
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Literal
Expand Down Expand Up @@ -96,16 +95,6 @@ def __init__(self, cfg: CameraCfg):
RuntimeError: If no camera prim is found at the given path.
ValueError: If the provided data types are not supported by the camera.
"""
# check if sensor path is valid
# note: currently we do not handle environment indices if there is a regex pattern in the leaf
# For example, if the prim path is "/World/Sensor_[1,2]".
sensor_path = cfg.prim_path.split("/")[-1]
sensor_path_is_regex = re.match(r"^[a-zA-Z0-9/_]+$", sensor_path) is None
if sensor_path_is_regex:
raise RuntimeError(
f"Invalid prim path for the camera sensor: {self.cfg.prim_path}."
"\n\tHint: Please ensure that the prim path does not contain any regex patterns in the leaf."
)
# perform check on supported data types
self._check_supported_data_types(cfg)
# initialize base class
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/isaaclab/sim/_impl/newton_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ def add_contact_sensor(
shape_names_expr: str | list[str] | None = None,
contact_partners_body_expr: str | list[str] | None = None,
contact_partners_shape_expr: str | list[str] | None = None,
prune_noncolliding: bool = False,
prune_noncolliding: bool = True,
verbose: bool = False,
):
"""Adds a contact view.
Expand Down
3 changes: 3 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
PHYSX_MESH_COLLISION_CFGS,
USD_MESH_COLLISION_CFGS,
activate_contact_sensors,
create_joint,
define_articulation_root_properties,
define_collision_properties,
define_deformable_body_properties,
Expand Down Expand Up @@ -76,6 +77,8 @@
"ArticulationRootPropertiesCfg",
"define_articulation_root_properties",
"modify_articulation_root_properties",
# joints
"create_joint",
# rigid bodies
"RigidBodyPropertiesCfg",
"define_rigid_body_properties",
Expand Down
97 changes: 97 additions & 0 deletions source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import isaaclab.sim.utils.prims as prim_utils
from isaaclab.sim import schemas
from isaaclab.sim.utils import bind_physics_material, bind_visual_material, clone
from isaaclab.sim.utils.stage import get_current_stage

if TYPE_CHECKING:
from . import shapes_cfg
Expand Down Expand Up @@ -252,6 +253,12 @@ def _spawn_geom_from_prim_type(
* ``{prim_path}/geometry`` - The prim that contains the mesh and optionally the materials if configured.
If instancing is enabled, this prim will be an instanceable reference to the prototype prim.

For fixed-base articulations (when ``articulation_props.fix_root_link=True``), we create a wrapper structure:

* ``{prim_path}`` - The root prim with ArticulationRootAPI (required to be parent of fixed joint).
* ``{prim_path}/root`` - The body prim with RigidBodyAPI and fixed joint to world.
* ``{prim_path}/root/geometry`` - The geometry prim with the mesh and collision.

Args:
prim_path: The prim path to spawn the asset at.
cfg: The config containing the properties to apply.
Expand Down Expand Up @@ -310,3 +317,93 @@ def _spawn_geom_from_prim_type(
# apply rigid body properties
if cfg.rigid_props is not None:
schemas.define_rigid_body_properties(prim_path, cfg.rigid_props)
# apply articulation root properties
if cfg.articulation_props is not None:
# Check if we need fixed-base wrapper structure
fix_root = getattr(cfg.articulation_props, "fix_root_link", False)
if fix_root:
_create_fixed_base_articulation_wrapper(prim_path, cfg.articulation_props)
else:
schemas.define_articulation_root_properties(prim_path, cfg.articulation_props)


def _create_fixed_base_articulation_wrapper(prim_path: str, articulation_props):
"""Create wrapper structure for fixed-base articulation.

Newton requires at least 2 bodies to recognize a fixed-base articulation.
This function adds a dummy body connected via a fixed joint.

The spawner already creates:
{prim_path}/
geometry/ <- RigidBodyAPI + collision

Structure after:
{prim_path}/
FixedJoint <- ArticulationRootAPI (body0=world, body1=geometry)
geometry/ <- RigidBodyAPI + collision (already exists)
fakebody/ <- RigidBodyAPI (no collision, minimal mass)
InternalJoint <- FixedJoint (body0=geometry, body1=fakebody)
"""
from pxr import UsdPhysics, Gf

stage = get_current_stage()
parent_prim = stage.GetPrimAtPath(prim_path)

# geometry prim already exists from the spawner
geometry_prim_path = prim_path + "/geometry"
geometry_prim = stage.GetPrimAtPath(geometry_prim_path)

# Ensure geometry has RigidBodyAPI (it should already have it)
if not geometry_prim.HasAPI(UsdPhysics.RigidBodyAPI):
UsdPhysics.RigidBodyAPI.Apply(geometry_prim)

# Remove RigidBodyAPI from parent if it exists (shouldn't be on both)
if parent_prim.HasAPI(UsdPhysics.RigidBodyAPI):
parent_prim.RemoveAPI(UsdPhysics.RigidBodyAPI)
applied_schemas = parent_prim.GetAppliedSchemas()
if "PhysxRigidBodyAPI" in applied_schemas:
parent_prim.RemoveAppliedSchema("PhysxRigidBodyAPI")

# Create fakebody prim with RigidBodyAPI and minimal mass
fakebody_prim_path = prim_path + "/fakebody"
prim_utils.create_prim(fakebody_prim_path, prim_type="Xform")
fakebody_prim = stage.GetPrimAtPath(fakebody_prim_path)
UsdPhysics.RigidBodyAPI.Apply(fakebody_prim)
mass_api = UsdPhysics.MassAPI.Apply(fakebody_prim)
mass_api.GetMassAttr().Set(0.001) # Minimal mass
mass_api.GetDiagonalInertiaAttr().Set(Gf.Vec3f(1e-6, 1e-6, 1e-6)) # Minimal inertia

# Create main FixedJoint from world to geometry
fixed_joint_prim = schemas.create_joint(
stage=stage,
joint_type="Fixed",
from_prim=None, # world
to_prim=geometry_prim,
joint_base_path=prim_path,
)

# Apply ArticulationRootAPI to the main FixedJoint
if fixed_joint_prim:
UsdPhysics.ArticulationRootAPI.Apply(fixed_joint_prim)

# Set articulation properties
from isaaclab.utils.string import to_camel_case
from isaaclab.sim.utils import safe_set_attribute_on_usd_prim

cfg_dict = articulation_props.to_dict()
cfg_dict.pop("fix_root_link", None)
for attr_name, value in cfg_dict.items():
if value is not None:
safe_set_attribute_on_usd_prim(
fixed_joint_prim, f"physxArticulation:{to_camel_case(attr_name)}", value, camel_case=False
)

# Create InternalJoint from geometry to fakebody
# This joint connects the two bodies, making Newton recognize the fixed-base structure
_ = schemas.create_joint(
stage=stage,
joint_type="Fixed",
from_prim=geometry_prim, # type: ignore[arg-type]
to_prim=fakebody_prim, # type: ignore[arg-type]
joint_base_path=fakebody_prim_path,
)
3 changes: 3 additions & 0 deletions source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ class RigidObjectSpawnerCfg(SpawnerCfg):
mass_props: schemas.MassPropertiesCfg | None = None
"""Mass properties."""

articulation_props: schemas.ArticulationRootPropertiesCfg | None = None
"""Articulation properties."""

rigid_props: schemas.RigidBodyPropertiesCfg | None = None
"""Rigid body properties.

Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/isaaclab/utils/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,7 @@ def compute_pose_error(
# Compute quaternion error (i.e., difference quaternion)
# Reference: https://personal.utdallas.edu/~sxb027100/dock/quaternion.html
# q_current_norm = q_current * q_current_conj
source_quat_norm = quat_mul(q01, quat_conjugate(q01))[:, 0]
source_quat_norm = quat_mul(q01, quat_conjugate(q01))[:, 3]
# q_current_inv = q_current_conj / q_current_norm
source_quat_inv = quat_conjugate(q01) / source_quat_norm.unsqueeze(-1)
# q_error = q_target * q_current_inv
Expand Down
Loading
Loading