Skip to content
Draft
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
112 changes: 112 additions & 0 deletions isaaclab_arena/assets/displayport_insertion_geometry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Copyright (c) 2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0

"""Geometry constants for the DisplayPort insertion connector assets."""

from __future__ import annotations

SOCKET_INSERTION_OFFSET = (0.0375, 0.0, 0.0)
"""Socket root-to-mate point offset in the socket local frame."""

PLUG_INSERTION_OFFSET = (0.0, 0.0, 0.0221)
"""Plug root-to-mate point offset in the plug local frame."""

PLUG_GOAL_ROT = (0.0, -0.70711, 0.0, 0.70711)
"""Plug orientation relative to the socket at the mated pose, in ``(x, y, z, w)`` order."""

PLUG_GOAL_ROT_INV = (0.0, 0.70711, 0.0, 0.70711)
"""Inverse of :data:`PLUG_GOAL_ROT`, in ``(x, y, z, w)`` order."""

DEFAULT_INSERTION_POINT = (0.0, 0.0, 0.1875)
"""Default mate point for the connector-only insertion scene."""

DEFAULT_SOCKET_ROT = (0.5, 0.5, 0.5, -0.5)
"""Default socket orientation with the opening facing upward."""

DEFAULT_PLUG_CLEARANCE_Z = 0.033
"""Default vertical plug clearance above the socket mate point."""

PASSIVE_DROP_SOCKET_POS = (0.0, 0.0, 0.15)
"""Socket root position used by the passive DisplayPort drop-test profile."""

PASSIVE_DROP_SOCKET_ROT = DEFAULT_SOCKET_ROT
"""Socket root orientation used by the passive DisplayPort drop-test profile."""

PASSIVE_DROP_PLUG_POS = (0.0, 0.0, 0.2096)
"""Plug root position used by the passive DisplayPort drop-test profile."""

PASSIVE_DROP_PLUG_ROT = (0.70711, 0.70711, 0.0, 0.0)
"""Plug root orientation used by the passive DisplayPort drop-test profile."""


def quat_rotate_vec(
q_xyzw: tuple[float, float, float, float], v_xyz: tuple[float, float, float]
) -> tuple[float, float, float]:
"""Apply an ``(x, y, z, w)`` quaternion rotation to a 3D vector."""
qx, qy, qz, qw = q_xyzw
vx, vy, vz = v_xyz
tx = 2.0 * (qy * vz - qz * vy)
ty = 2.0 * (qz * vx - qx * vz)
tz = 2.0 * (qx * vy - qy * vx)
return (
vx + qw * tx + qy * tz - qz * ty,
vy + qw * ty + qz * tx - qx * tz,
vz + qw * tz + qx * ty - qy * tx,
)


def quat_mul(
lhs_xyzw: tuple[float, float, float, float],
rhs_xyzw: tuple[float, float, float, float],
) -> tuple[float, float, float, float]:
"""Multiply two quaternions in ``(x, y, z, w)`` order."""
x1, y1, z1, w1 = lhs_xyzw
x2, y2, z2, w2 = rhs_xyzw
return (
w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,
w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2,
w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
)


def compute_socket_root(
geometry_pos: tuple[float, float, float],
socket_rot: tuple[float, float, float, float],
) -> tuple[float, float, float]:
"""Compute the socket USD root position from a desired mate-point position."""
rotated = quat_rotate_vec(socket_rot, SOCKET_INSERTION_OFFSET)
return (
geometry_pos[0] - rotated[0],
geometry_pos[1] - rotated[1],
geometry_pos[2] - rotated[2],
)


def compute_plug_pose(
geometry_pos: tuple[float, float, float],
socket_rot: tuple[float, float, float, float],
z_clearance: float = 0.0,
) -> tuple[tuple[float, float, float], tuple[float, float, float, float]]:
"""Compute plug USD root pose from a desired mate-point position and socket rotation."""
plug_rot = quat_mul(socket_rot, PLUG_GOAL_ROT)
plug_offset_world = quat_rotate_vec(plug_rot, PLUG_INSERTION_OFFSET)
plug_root = (
geometry_pos[0] - plug_offset_world[0],
geometry_pos[1] - plug_offset_world[1],
geometry_pos[2] - plug_offset_world[2] + z_clearance,
)
return plug_root, plug_rot


DEFAULT_SOCKET_ROOT_POS = compute_socket_root(DEFAULT_INSERTION_POINT, DEFAULT_SOCKET_ROT)
"""Default socket USD root position for the connector-only insertion scene."""

DEFAULT_PLUG_ROOT_POS, DEFAULT_PLUG_ROT = compute_plug_pose(
DEFAULT_INSERTION_POINT,
DEFAULT_SOCKET_ROT,
z_clearance=DEFAULT_PLUG_CLEARANCE_Z,
)
"""Default plug USD root pose for the connector-only insertion scene."""
5 changes: 4 additions & 1 deletion isaaclab_arena/assets/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __init__(
initial_pose: Pose | None = None,
relations: list[RelationBase] = [],
spawner_cfg: SpawnerCfg | None = None,
activate_contact_sensors: bool | None = None,
**kwargs,
):
# Pull out addons (and remove them from kwargs before passing to super)
Expand All @@ -57,6 +58,7 @@ def __init__(
self.initial_pose = initial_pose
self.relations = list(relations)
self.reset_pose = True
self.activate_contact_sensors = activate_contact_sensors
self.spawn_cfg_addon = spawn_cfg_addon
self.asset_cfg_addon = asset_cfg_addon
self.bounding_box = None
Expand Down Expand Up @@ -142,9 +144,10 @@ def _get_spawn_cfg(self, activate_contact_sensors: bool = False):

def _generate_rigid_cfg(self) -> RigidObjectCfg:
assert self.object_type == ObjectType.RIGID
activate_contact_sensors = True if self.activate_contact_sensors is None else self.activate_contact_sensors
object_cfg = RigidObjectCfg(
prim_path=self.prim_path,
spawn=self._get_spawn_cfg(activate_contact_sensors=True),
spawn=self._get_spawn_cfg(activate_contact_sensors=activate_contact_sensors),
**self.asset_cfg_addon,
)
return self._add_initial_pose_to_cfg(object_cfg)
Expand Down
79 changes: 79 additions & 0 deletions isaaclab_arena/assets/object_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@


from abc import ABC
from pathlib import Path
from typing import TYPE_CHECKING, Any

import isaaclab.sim as sim_utils
Expand All @@ -20,6 +21,7 @@
from isaaclab_arena.affordances.placeable import Placeable
from isaaclab_arena.affordances.pressable import Pressable
from isaaclab_arena.affordances.turnable import Turnable
from isaaclab_arena.assets import displayport_insertion_geometry as displayport_geometry
from isaaclab_arena.assets.lightwheel_lazy import LightwheelLazyPath
from isaaclab_arena.assets.nucleus import ARENA_NUCLEUS_DIR
from isaaclab_arena.assets.object import Object
Expand All @@ -32,6 +34,11 @@
from isaaclab_arena.assets.register import register_asset
from isaaclab_arena.utils.pose import Pose

_LOCAL_ASSET_DIR = Path(__file__).resolve().parent / "usd"
_DISPLAYPORT_ASSET_DIR = _LOCAL_ASSET_DIR / "displayport_insertion"
_DISPLAYPORT_PLUG_USD = str(_DISPLAYPORT_ASSET_DIR / "display_port_plug_fixed_sdf.usd")
_DISPLAYPORT_SOCKET_USD = str(_DISPLAYPORT_ASSET_DIR / "display_port_socket_fixed_sdf_noprotrusions.usd")


class LibraryObject(Object):
"""
Expand All @@ -46,17 +53,22 @@ class LibraryObject(Object):
scale: tuple[float, float, float] = (1.0, 1.0, 1.0)
spawn_cfg_addon: dict[str, Any] = {}
asset_cfg_addon: dict[str, Any] = {}
activate_contact_sensors: bool | None = None

def __init__(
self,
instance_name: str | None = None,
prim_path: str | None = None,
initial_pose: Pose | None = None,
scale: tuple[float, float, float] | None = None,
activate_contact_sensors: bool | None = None,
**kwargs,
):
name = instance_name if instance_name is not None else self.name
scale = scale if scale is not None else self.scale
activate_contact_sensors = (
self.activate_contact_sensors if activate_contact_sensors is None else activate_contact_sensors
)
super().__init__(
name=name,
prim_path=prim_path,
Expand All @@ -65,6 +77,7 @@ def __init__(
object_type=self.object_type,
scale=scale,
initial_pose=initial_pose,
activate_contact_sensors=activate_contact_sensors,
spawn_cfg_addon=self.spawn_cfg_addon,
asset_cfg_addon=self.asset_cfg_addon,
**kwargs,
Expand Down Expand Up @@ -368,6 +381,72 @@ def __init__(
)


@register_asset
class DisplayPortPlug(LibraryObject):
"""Right-angle DisplayPort plug used by connector insertion simulations."""

name = "displayport_plug"
tags = ["object", "connector", "displayport"]
usd_path = _DISPLAYPORT_PLUG_USD
activate_contact_sensors = True
spawn_cfg_addon = {
"rigid_props": sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
kinematic_enabled=False,
max_depenetration_velocity=0.5,
linear_damping=0.0,
angular_damping=0.0,
max_linear_velocity=1000.0,
max_angular_velocity=3666.0,
enable_gyroscopic_forces=True,
solver_position_iteration_count=128,
solver_velocity_iteration_count=1,
max_contact_impulse=None,
),
"mass_props": sim_utils.MassPropertiesCfg(mass=0.03),
"collision_props": sim_utils.CollisionPropertiesCfg(contact_offset=0.00001, rest_offset=-0.00005),
}
asset_cfg_addon = {
"init_state": RigidObjectCfg.InitialStateCfg(
pos=displayport_geometry.DEFAULT_PLUG_ROOT_POS,
rot=displayport_geometry.DEFAULT_PLUG_ROT,
),
}


@register_asset
class DisplayPortSocket(LibraryObject):
"""Fixed DisplayPort socket used by connector insertion simulations."""

name = "displayport_socket"
tags = ["object", "connector", "displayport"]
usd_path = _DISPLAYPORT_SOCKET_USD
activate_contact_sensors = False
spawn_cfg_addon = {
"rigid_props": sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
kinematic_enabled=True,
max_depenetration_velocity=5.0,
linear_damping=0.0,
angular_damping=0.0,
max_linear_velocity=1000.0,
max_angular_velocity=3666.0,
enable_gyroscopic_forces=True,
solver_position_iteration_count=128,
solver_velocity_iteration_count=1,
max_contact_impulse=1e32,
),
"mass_props": sim_utils.MassPropertiesCfg(mass=None),
"collision_props": sim_utils.CollisionPropertiesCfg(contact_offset=0.0001, rest_offset=-0.0001),
}
asset_cfg_addon = {
"init_state": RigidObjectCfg.InitialStateCfg(
pos=displayport_geometry.DEFAULT_SOCKET_ROOT_POS,
rot=displayport_geometry.DEFAULT_SOCKET_ROT,
),
}


class LightBase(LibraryObject, ABC):
"""Abstract base for spawnable lights.

Expand Down
Binary file not shown.
Binary file not shown.
6 changes: 6 additions & 0 deletions isaaclab_arena/environments/arena_env_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,12 @@ def compose_manager_cfg(self) -> tuple[IsaacLabArenaManagerBasedRLEnvCfg, dict[s
# Apply the requested physics backend after the callback so it remains the final authority.
presets = self.cfg.presets
if presets is not None:
if not self.arena_env.allow_physics_presets:
raise NotImplementedError(
f"Environment {self.arena_env.name!r} defines task-specific physics settings and does not support "
f"builder physics presets; got --presets {presets}."
)

from isaaclab_arena.environments.isaaclab_arena_manager_based_env_cfg import ArenaPhysicsCfg

env_cfg.sim.physics = getattr(ArenaPhysicsCfg(), presets)
Expand Down
3 changes: 3 additions & 0 deletions isaaclab_arena/environments/isaaclab_arena_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def __init__(
rl_policy_cfg: str | None = None,
episode_recorder_terms: dict[str, EpisodeRecorderTermCfg] | None = None,
placer_params: ObjectPlacerParams | None = None,
allow_physics_presets: bool = True,
):
"""
Args:
Expand All @@ -54,6 +55,7 @@ def __init__(
built-in ones, keyed by name.
placer_params: Object placement configuration. When None, default
ObjectPlacerParams are used.
allow_physics_presets: Whether builder-level physics presets may override simulation physics.
"""
self.name = name
self.scene = scene
Expand All @@ -67,3 +69,4 @@ def __init__(
self.rl_policy_cfg = rl_policy_cfg
self.episode_recorder_terms = episode_recorder_terms or {}
self.placer_params = placer_params
self.allow_physics_presets = allow_physics_presets
Loading
Loading