ADR-0045: NVIDIA Isaac Sim as an optional sim backend
- Status: Proposed
- Date: 2026-06-10
- Related: ADR-0002 (eval & sim environments);
ADR-0031 (
build_halsim/real seam); ADR-0033 (robot-parameterised native scenes); ADR-0034 (deploy-sim scene-attach); ADR-0012 (open-core licensing posture). - ADR number note:
0043is an unfilled gap indocs/adr/(renumber-in-flight). This ADR claims0045(next after the highest present,0044) to avoid colliding with whatever lands at0043.
Context
OpenRAL drives VLA-policy rollouts through a single minimal scene seam. Every simulator —
native MuJoCo (tabletop_push, openarm_robosuite), LIBERO, MetaWorld, RoboCasa, ALOHA,
PushT, ManiSkill3 (SAPIEN), SimplerEnv — is a SimRollout factory registered under a
scene_id in openral_sim.registry.SCENES. openral sim run --config <scene>.yaml
resolves the factory and runs the episode loop; openral deploy sim wraps the same
SimRollout in a ROS lifecycle node via SimAttachedHAL (ADR-0034).
There is recurring interest in NVIDIA Isaac Sim (PhysX + RTX rendering + USD, on the
Omniverse Kit platform) for photoreal rendering, GPU-parallel rollouts, and USD asset
pipelines. The scaffolding already anticipates it: PhysicsBackend.ISAACSIM = "isaacsim"
exists in openral_core.schemas (tagged "Future"). This ADR records how Isaac Sim
would integrate and the two judgment calls that must be settled before code is written,
because both cross a layer boundary (a new sim backend) and pull in a closed dependency
(CLAUDE.md §3, §1.9).
What the seam already gives us (the easy half)
The integration surface is small and well-precedented (ManiSkill3 and SimplerEnv are both non-MuJoCo, free-axis backends following the same path):
SimRolloutProtocol (python/sim/src/openral_sim/rollout.py) — four methods:Optional duck-typed extensions (@runtime_checkable class SimRollout(Protocol): scene: SceneSpec task: TaskSpec def reset(self, seed: int | None = ...) -> Observation def step(self, action: NDArray[np.float32]) -> StepResult def render(self) -> NDArray[np.uint8] | None def close(self) -> Nonemujoco_handles,viewer_render,enable_intrinsic_viewer) are not part of the Protocol; Isaac Sim implements none of them (it has no MuJoCo handles and a self-managed viewport).- One registration —
@SCENES.register("isaac_sim", fixed_robot=None)in a newpython/sim/src/openral_sim/backends/isaac_sim.py, plus an import line inbackends/__init__._register_backends(). - Schema —
PhysicsBackend.ISAACSIMalready exists; no schema change. - Dependency isolation — a new
isaacsimgroup inpyproject.toml, mirroring the per-backendlibero/robocasa/maniskill3groups. - Control — Isaac Lab ships first-class OSC (
OperationalSpaceControllerActionCfg) and differential-IK (DifferentialIKControllerActionCfg) end-effector action terms, matching the robosuite-OSC convention OpenRAL already mandates for new arm scenes.
The hard half is runtime, not plumbing, and is captured in the two decisions below.
Decision
Integrate Isaac Sim as an optional, externally-provisioned, free-axis SimRollout
backend, subject to the two constraints below. Isaac Lab (not raw Isaac Sim) is the
integration target: its ManagerBasedRLEnv subclasses gymnasium.Env and provides the
reset/step/observation/reward/termination managers we adapt to SimRollout.
Decision 1 — Process model: out-of-process Python 3.11 sidecar
Isaac Sim wheels are hard-pinned per interpreter (4.x→py3.10, 5.x→py3.11, 6.x→py3.12).
OpenRAL pins >=3.12,<3.13, so an in-process backend can only use Isaac Sim 6.x —
the newest, least-proven line — and must share one venv with the VLA torch/CUDA stack
(known LD_PRELOAD/libgomp OpenMP clash) under the rigid SimulationApp-before-omni.*
import ordering.
Decision: adopt an out-of-process sidecar. A long-lived Isaac Sim process in its own
py3.11 environment running Isaac Lab 5.1 (the mature line), fronted by an in-process
IsaacSimEnv (SimRollout) that marshals reset/step/render over an IPC channel —
the same out-of-process isolation pattern OpenRAL already uses for the LocateAnything
detector rSkill. This decouples Isaac's interpreter and torch/CUDA stack from OpenRAL's,
sidesteps the import-order and OpenMP constraints, lets us pin the mature 5.1 line
independent of the repo's 3.12 interpreter, and amortises the tens-of-seconds Omniverse Kit
startup across an episode instead of paying it per test.
The in-process 6.x-on-3.12 single-env path is explicitly deferred as a documented future fallback, to be revisited only if 6.x matures and the IPC overhead proves limiting; it is not implemented under this ADR.
Decision 2 — Licensing: externally provisioned, never vendored
Isaac Lab is BSD-3 and Isaac Sim source is Apache-2.0 (both compatible with open-core), but the bundled Omniverse Kit SDK, models, and textures ship under the proprietary "NVIDIA Isaac Sim Additional Software and Materials License," which restricts redistribution. Per CLAUDE.md §1.9 / §3 ("Closed-SDK code is not bundled in open-core"), Isaac Sim is treated like GR00T weights and other closed components:
- Never vendored. No Omniverse Kit binaries, USD assets, or wheels checked into the
repo. The
isaacsimdependency group documents the NVIDIA index + license requirement; it does not bundle the artifacts. - Install-time guard. The factory lazy-imports the SDK and raises a typed
ROSConfigErrorwith a provisioning hint when absent — failing closed, never silently degrading. No commercial-deployment guard beyond NVIDIA's own terms is added here; the posture is "external dependency the user provisions," matching ADR-0012.
Backend shape
# python/sim/src/openral_sim/backends/isaac_sim.py
@SCENES.register("isaac_sim", fixed_robot=None) # free-axis: any manifest robot
def _build_isaac_sim(env_cfg: SimEnvironment) -> SimRollout:
try:
from openral_sim.backends._isaac_bridge import IsaacSidecarClient # lazy
except ImportError as exc:
raise ROSConfigError(
"Isaac Sim backend unavailable. Provision NVIDIA Isaac Sim / Isaac Lab "
"(separate license, RTX GPU) and `uv sync --group isaacsim`."
) from exc
return IsaacSimEnv(env_cfg, client=IsaacSidecarClient.launch(env_cfg))
Observation contract is unchanged: dict with images (HWC uint8 RGB per camera),
state (1-D float32 proprioception), task (instruction str). The sidecar marshals Isaac
Lab's GPU tensors to numpy and unbatches the num_envs=1 leading dim before returning.
Safety posture
No safety-kernel, E-stop, or velocity-limit code is touched. Isaac Sim is a simulation
backend behind the existing SimRollout/SimAttachedHAL seam (ADR-0034); the
real-hardware exclusion in build_hal (mode="real" + scene attach → ROSConfigError)
already prevents a sim scene from ever attaching to physical actuators. No new flag disables
or bypasses any safety check.
Alternatives considered
- In-process Isaac Sim 6.x on py3.12. Rejected as the default: forces the newest/least
proven line, shares one venv with the VLA torch stack (OpenMP/
LD_PRELOADclash), and imposesSimulationApp-first import ordering on the whole process. Kept as a documented future fallback once 6.x matures. - Relax the repo's
>=3.12,<3.13pin to allow py3.11. Rejected: a repo-wide interpreter change to accommodate one optional backend is disproportionate and crosses far more than the sim layer. - Raw Isaac Sim (no Isaac Lab). Rejected: we would hand-roll the env/MDP managers that
Isaac Lab already provides as a
gymnasium.Env; more code, less standard. - Don't integrate; stay on MuJoCo/SAPIEN. The status-quo option. Valid until a concrete need for RTX photoreal rendering, USD pipelines, or GPU-parallel rollouts materialises — this ADR makes the path ready without committing implementation effort prematurely.
Consequences
- Positive: RTX photoreal rendering, USD assets, and GPU-parallel rollouts become available behind the existing scene seam; the OSC/diff-IK control surface maps cleanly to the robosuite-OSC convention; out-of-process isolation keeps Isaac's heavy stack from contaminating the core env.
- Negative / costs: RTX-only GPUs (datacenter A100/H100 unsupported), ~50 GB install,
tens-of-seconds Kit startup (mitigated by the long-lived sidecar), an IPC marshalling
layer to build and test, and a closed dependency that can never be vendored. Sim tests
for this backend will not fit the
<10 minbudget on a cold CI runner without a GPU-equipped self-hosted runner; absent one, they take the legitimatepytest.skippath (CLAUDE.md §1.12). - Follow-up work (separate PRs, gated on this ADR): (a)
_isaac_bridgesidecar + IPC; (b)IsaacSimEnvSimRolloutadapter + registration; (c)isaacsimdependency group; (d) an examplescenes/sim/isaac_sim_*.yaml; (e)tests/sim/coverage on a self-hosted RTX runner; (f)docs/METHODS.md+ repo-state-map updates.
Implementation note — PoC built & verified 2026-06-10
A working proof-of-concept landed on branch feat/isaac-sim and was run for real
on an RTX 4070 Laptop (8 GB):
- Sidecar venv: py3.11 with
isaacsim[all]==5.1.0.0+isaaclab==2.3.2+ pyzmq + msgpack (188 packages). Install gotchas worth recording:flatdict(anisaaclabdep) needs--no-build-isolation-package flatdict+ a pre-installedsetuptools<80; the multi-GB Isaac wheels need a raisedUV_HTTP_TIMEOUT; cross-index resolution needs--index-strategy unsafe-best-match(isaacsim onpypi.nvidia.com, isaaclab on PyPI). - Isaac Sim core, not Isaac Lab, for the env. The PyPI
isaaclabwheel does not ship theisaaclab.sim/isaaclab.envstask machinery (import isaaclab.sim→ModuleNotFoundError); those need the git-source install (./isaaclab.sh) plus the PyPI-absentisaaclab_assets/isaaclab_tasks. The Isaac Sim core API (isaacsim.core.api.World,isaacsim.robot.manipulators.examples.franka.Franka,isaacsim.sensors.camera.Camera) is fully present and is what the PoC scene (tools/isaac_scene.py) uses. Wiring the full Isaac Lab manager-based env (OSC/diff-IK action terms, task MDP) is deferred to a follow-up that provisions the source install — it does not change the sidecar architecture. - Verified end-to-end: openral (py3.12) auto-spawns the sidecar, which boots
a headless Omniverse Kit app (~10 s warm; first boot ~6 min to fill the
extension cache), builds a Franka + cube + RTX camera scene, and answers
reset/step/renderover ZMQ.resetreturns a(128,128,3)RGB frame + 12-D state;stepadvances real PhysX and returnscube_zreward;renderreturns a non-trivial RTX frame (99.9 % non-zero pixels). Covered bytests/sim/test_franka_random_isaac.py(real GPU, skips without the sidecar venv) andtests/unit/test_isaac_sim_sidecar_wire.py(wire codec against a real ZMQ boundary, no GPU). - 8 GB-GPU gotcha: constructing the core
World(device="cuda:0")forces the GPU PhysX pipeline, whose firstreset/stepwarmup hangs for minutes on an 8 GB laptop GPU. The default-deviceWorld(stage_units_in_meters=1.0)renders the same scene in ~15 s. The PoC uses the default; GPU PhysX is a follow-up tuning knob. Also: never run two Kit apps concurrently — a second app disables the shared kvdb and starves the first (observed as a stuck boot). - EULA: the sidecar sets
OMNI_KIT_ACCEPT_EULA=YES(user's acceptance of the proprietary NVIDIA Omniverse license) — reinforces §Decision-2's "externally provisioned, never vendored" posture.
Follow-up — Isaac-Lab-free control + LIBERO-shaped scene + real VLA (2026-06-10)
A second iteration confirmed Isaac Lab is not needed even for end-effector
control, and ran a real rSkill through openral sim run:
- Moving the robot / OSC needs neither Isaac Lab nor its OSC term. Arm motion
is
ArticulationController.apply_actionon the coreFranka. End-effector control uses the coreisaacsim.robot_motion.motion_generationLula kinematics solver (LulaKinematicsSolver+ArticulationKinematicsSolveron theright_gripperframe): position-delta IK returns joint targets and the EE tracks them (verified: ee z 0.39→0.34→0.29 for −0.05/−0.10 deltas). Isaac Lab'sOperationalSpaceControlleris only a convenience; it is not the sole OSC path. tools/isaac_bowl_plate_scene.py— a table + YCB024_bowlUSD + plate + Franka scene mirroring the LIBERO contract (camera1/camera2 + 8-D[eef_pos‖axisangle‖gripper_qpos]state, 7-D OSC-pose-delta action via Lula IK). Selected by the sidecar--layout bowl_plate(scenes/sim/isaac_franka_bowl_plate.yaml). Assets confirmed reachable on the Isaac S3 nucleus (table ✓,024_bowl✓; no plate USD ships → primitive).- Real VLA verified:
openral sim run --config scenes/sim/isaac_franka_bowl_plate.yaml --rskill rskills/act-liberoran the full pipeline — rSkill compat check → ACT weights from HF → Isaac sidecar → ACT consumes camera1/camera2 + 8-D state → 7-D actions → Lula IK drives the Franka — for 200 steps at ~15 ms/step.success=Falseis expected (the bowl/plate task is out-of-distribution for a LIBERO policy); the check is that the pipeline runs and the arm is driven, not task success. ACT is preferred on an 8 GB GPU (tiny next to the ~2 GB Isaac sidecar).
Follow-up — openral deploy sim minimal bring-up (2026-06-10)
deploy sim wraps the scene in SimAttachedHAL and runs the ROS lifecycle
stack. An audit found the path is mostly backend-agnostic; the one hard
blocker was that SimAttachedHAL._probe_env_action_dim reads env.action_dim
and _IsaacSimSidecar didn't expose it → ROSConfigError at connect().
Minimal bring-up (this PR): _IsaacSimSidecar gains an action_dim property
that reads the value the sidecar's ping already returns (8 for lift_cube,
7 for bowl_plate) — no sidecar change. With it, SimAttachedHAL connects,
read_images() flows the RTX frames to the sensor bridge, and send_action()
steps the env. The deploy scene is scenes/deploy/isaac_franka.yaml (env-only
DeployScene, lift_cube layout: its 8-D action matches the franka_panda
manifest's 8 joints + JOINT_POSITION mode, so a HAL action packs to
env_action_dim=8 with no n_dof gap — cf. ADR-0036). Covered in-process by
tests/sim/test_franka_isaac_deploy_hal.py (real sidecar; no ROS launch needed).
Backend-agnostic joint-state + idle-step (RESOLVED — ADR-0034 amendment
2026-06-10): SimAttachedHAL no longer needs a MuJoCo handle to be useful to a
non-MuJoCo backend:
- read_state() sources real joint angles from obs["joint_positions"] (the
Isaac scenes emit it via IsaacSceneBase._joint_positions() — the Franka's
9 DOF mapped to the manifest's 8 joints); /joint_states carries live values,
not zeros. Falls back to zeros only when a backend provides none. This helps
ManiSkill3 / SimplerEnv too.
- The idle stepper drops the MuJoCo-handle gate, so Isaac cameras stay live when
idle. The method-only-on-SimAttachedHAL exclusion remains the safety
guarantee. Verified by tests/unit/test_sim_attached_non_mujoco.py (no GPU)
+ the live deploy HAL test (real non-zero joint values).
Full openral deploy sim run verified (2026-06-10). The complete ROS graph
was brought up against the Isaac scene
(openral deploy sim --config scenes/deploy/isaac_franka.yaml --no-enable-octomap
--hal viewer_enabled=false): safety kernel + reasoner + prompt-router + the
openral_hal_franka lifecycle node all reached active; the HAL connected to
the auto-spawned Isaac sidecar, SimSensorBridge published the cameras, the
sim-only idle stepper @ 10 Hz started (proving the de-gated idle path runs for
a non-MuJoCo backend), and /joint_states carried the real Franka rest pose
(panda_joint2≈−0.52, joint4≈−2.86, joint6≈3.04, joint7≈0.74), not zeros.
Bug surfaced + fixed by that run: SidecarClient._spawn inherited the parent's
PYTHONPATH. openral deploy sim injects this py3.12 venv's site-packages onto
PYTHONPATH, which then shadowed the py3.11 Isaac venv's own numpy in the
spawned sidecar → No module named 'numpy._core._multiarray_umath' at boot. Fix:
strip PYTHONPATH / VIRTUAL_ENV from the sidecar child env (it is
self-contained). Operational notes: the reasoner needs an LLM env
(OPENRAL_REASONER_LLM_*; a local ollama openai-compatible endpoint suffices to
activate); run with OPENRAL_AUTO_INSTALL_DEPS=0 and pyzmq/msgpack present on the
openral venv so the isaac_client probe doesn't trigger a uv sync.
Previously-deferred items, now RESOLVED (2026-06-10):
- Joint velocities — the Isaac scenes now also emit obs["joint_velocities"]
(franka_joint_velocities() maps the 9 DOF → 8); read_state populates
JointState.velocity from it for non-MuJoCo backends (zeros fallback retained).
- 2-camera deploy + camera-VLA action contract — there was no real gap:
SimAttachedHAL (the scene-attach path) packs purely on action.control_mode
and does not gate on the manifest's supported_control_modes (only
_mujoco_arm.py does). So a LIBERO franka rSkill dispatching as
CARTESIAN_DELTA (env_action_dim=7, ADR-0036) drives the bowl_plate Isaac
scene exactly as it drives the LIBERO MuJoCo scene — the 7-D vector's pos-delta
lands at slots 0–2 and the gripper at slot 6, which is what the scene reads.
scenes/deploy/isaac_franka_bowl.yaml (bowl_plate, two cameras) is the deploy
scene for it — no missing-camera warning. Verified live: the full deploy-sim
graph on that scene + ros2 action send_goal /openral/execute_rskill for
OpenRAL/rskill-act-libero ran to a SUCCEEDED result — the VLA loaded, consumed
the live Isaac state_to_policy, emitted 7-D actions the HAL dispatched as
mode=cartesian_delta env_dim=7 (+ gripper_position for the gripper slot),
and the bowl_plate scene drove the Franka via Lula IK under the safety kernel's
active self-collision check — no E-stop.
Amendment — Robot-agnostic, URDF-driven scenes (2026-06-11)
Problem found. Every Isaac scene built so far (lift_cube in tools/isaac_scene.py,
bowl_plate in tools/isaac_bowl_plate_scene.py) hardcodes Isaac's built-in Franka
example USD asset:
from isaacsim.robot.manipulators.examples.franka import Franka
self._franka = self._world.scene.add(Franka(prim_path="/World/Franka", name="franka"))
The sidecar already accepts --robot and isaac_sim._build_isaac_sim_scene forwards
env_cfg.robot_id, but the geometry ignores it — the scene is a Franka regardless of the
manifest, and the DOF↔manifest mapping (_franka_dof_to_manifest in tools/_isaac_scene_base.py)
is Franka-specific. This contradicts the DeployScene contract, under which a scene is
environment + backend and the robot is pluggable from its RobotDescription — exactly
how MuJoCo/robosuite native scenes already work (ADR-0033 robot-parameterised native scenes;
ADR-0034 deploy-sim scene-attach). The base ADR even registers the backend fixed_robot=None
("free-axis: any manifest robot") — the intent was always robot-agnostic; the PoC just took the
Isaac-example-asset shortcut to get PhysX + RTX up.
Goal. Bring up any manifest robot in Isaac — starting with panda_mobile — and have it
emit the correct ROS topics/controllers for rSkills (/joint_states, per-camera RGB, depth
PointCloud2, /scan, /odom, control-mode dispatch), so a user defining their own
RobotDescription gets a working Isaac scene without bespoke per-robot Isaac code and
without fabricating sensors the robot does not declare.
Design — IsaacManifestScene built from a marshaled robot spec
The sidecar runs py3.11 and cannot import openral_core, so the robot-agnostic path cannot
pass a RobotDescription object across the boundary. Instead:
- Marshal the manifest to a plain-JSON "isaac robot spec." The openral-side backend
(
isaac_sim.py, py3.12) resolves theRobotDescriptionand serializes only what the scene needs to a temp JSON file, passed via a new--robot-spec <path>CLI arg: urdf_path— the manifest'surdf_pathresolved to an on-disk file on the py3.12 side (thepython:robot_descriptions.<pkg>:URDF_PATHform resolves whererobot_descriptionslives — the sidecar venv need not carry it);joints— ordered[{name, role, sim_joint_name?}]so the scene maps Isaac articulation DOF ↔ manifest joint order generically, retiring_franka_dof_to_manifest;base_joints—[forward, side, yaw]when the embodiment has a planar base, else absent;sensors— eachSensorSpec(name,modality∈ rgb|depth|lidar_2d,frame_id,parent_frame,intrinsics,range_min_m/range_max_m,vla_feature_key);-
action—{dim, control_mode}from the action contract. -
Import the URDF → USD articulation. Replace
world.scene.add(Franka(...))with Isaac Sim's URDF importer extension (isaacsim.asset.importer.urdf, the 5.1-line successor toomni.importer.urdf) convertingurdf_pathinto a USD articulation prim. Exact importer class/config is settled against the provisioned 5.1 install at implementation time — the base ADR set the precedent of verifying Isaac APIs by running, not guessing (truth-over-plausibility, CLAUDE.md §1.2). -
Generic articulation controller. Drive the imported articulation through the core
ArticulationController.apply_action(already used byIsaacLiftScene), but index targets by the spec's joint order + role (arm/gripper/base) rather than the Franka 7+2 layout. The action→joint mapping keys onaction.control_mode— JOINT_POSITION direct, CARTESIAN_DELTA via the core Lula IK already proven in the base ADR, BODY_TWIST for the base — mirroringSimAttachedHAL's existing control-mode dispatch. -
Generic sensors from the spec — one attachment per declared
SensorSpec, nothing more: - rgb →
isaacsim.sensors.camera.Camera+get_rgba()(today's path), keyed by the sensor'svla_feature_keyintoimages; - depth → the same
Camerawith thedistance_to_image_planeannotator → a depth array fed to the existingopenral_sim.backends.depth_camera.synthesize_depth_pointcloud(the bridge is already backend-agnostic; only the array source changes from MuJoCo ray-cast to the Isaac annotator) → the/…/pointsPointCloud2octomap consumes; -
lidar_2d → an Isaac RTX lidar (or a rotating ray-cast at base height) → the synthetic
/scanthe panda_mobile HAL already expects for Nav2 + slam_toolbox. A modality the manifest does not declare is not created —franka_panda(no depth, no lidar) gets neither; onlypanda_mobile(which genuinely declaresfront_depth+base_scan) gets them. This is the explicit constraint: never add a sensor a robot does not have. -
Mobile base — kinematic (decided 2026-06-11).
panda_mobile'surdf_pathis the Panda arm only (panda_description); its 3-DOF holonomic base is manifest-defined (base_joints), not in the URDF, and exists nowhere as an Isaac-importable asset (robosuite composes base+arm at the MJCF layer; the MJCF importer would have to ingest a runtime-composed, controller-coupled model). Two options were weighed — a PhysX-articulated base (real prismatic-x / prismatic-y / revolute-yaw joints on the imported arm root) vs a kinematic base — and we chose kinematic: import the armfix_base=Trueand teleport the whole articulation root each step from a base-frame-twist-integrated(x, y, yaw)pose, surfacing abase_posefor/odom. Rationale: robust (no fragile USD articulation surgery), delivers exactly what the goal needs — correct/joint_states(base joints filled from the pose), base motion, and/odomfor rSkills — and base obstacle avoidance is Nav2's 2-D costmap job, not PhysX's. The base joints are NOT URDF DOFs; their/joint_statesvalues come from the kinematic pose. Data-driven by the spec'sbase_joints— not a hardcoded robot name.
Incremental milestones (separate commits, gated on this amendment)
- M1 (DONE, verified live) —
IsaacManifestSceneimportsfranka_pandafrom its URDF (fixed arm, single RGB camera, JOINT_POSITION controller), replacing the hardcodedFrankaexample asset.tests/sim/test_franka_urdf_isaac.py:/joint_statescarries the imported arm's live pose and a JOINT_POSITION action drives it. - M3-base (DONE, verified live) —
panda_mobileon the same path: kinematic holonomic base (11-D action = 7 arm + 1 gripper + 3 base-twist), 11-joint/joint_states(3 base + 7 arm + 1 gripper) with base joints filled from the kinematic pose, and a forwardbase_twistthat moves the base.tests/sim/test_panda_mobile_isaac.py. - HAL base generalization (DONE, verified live) —
SimAttachedHAL(the deploy-sim HAL) was MuJoCo-coupled for the mobile base; generalized in place (no Mujoco/Isaac subclass split — that stays the last resort):base_posereadsobs["base_pose"]without a MuJoCo handle, and aBODY_TWIST(the/cmd_velbridge's output) routes through_apply_body_twist_via_env_step→env.stepinstead of raising.tests/sim/test_panda_mobile_isaac_hal.py: through the same HAL, aBODY_TWISTmoves the Isaac base,base_pose/base_twistfeed/odom,/joint_statestracks it. - Full deploy-sim ROS graph (DONE, verified live 2026-06-11) —
openral deploy sim --config scenes/deploy/isaac_panda_mobile_urdf.yamlbrought up the complete graph on the Isaacpanda_mobilescene (no MuJoCo anywhere): theopenral_hal_panda_mobilelifecycle node activated @30 Hz wrapping the IsaacSimAttachedHAL; the C++safety_kernelarmed (n_dof=11, 12-link self-collision, ADR-0040 velocity+cartesian); thereasoneractivated with a 3-skill palette matching the robot;runtime_node(WorldState) ran. Live topics confirmed:/joint_states(11 joints, real imported-Panda arm pose),/odom+ TFodom→base_link, three/openral/cameras/*publishers (camera1@10 Hz),/scan@10 Hz. End-to-end actuation: a/cmd_vellinear.x=0.3moved/odomx0.0 → 0.315 m(and/joint_statesbase_xtracked it) — the full chain/cmd_vel → MobileBaseBridge → BODY_TWIST → _apply_body_twist_via_env_step → Isaac kinematic base → obs["base_pose"] → /odom, under the active safety kernel, no E-stop. Run with--no-enable-slam/nav2/octomap(the perception-richness leg below) but the HAL/control/safety/odom graph the robot-agnostic goal targets is proven. - Multi-RGB + depth → octomap (DONE, verified live) — the scene now renders one base-relative
camera per manifest
SensorSpec:camera1/2/3(256×256 RTX frames) + a forward-facingfront_depth. Depth uses Isaac'sCamera.get_pointcloud(world_frame=True)(Isaac owns the camera convention — no optical-frame guess) transformed world→base_link by the kinematic base pose, surfaced asobs["depth_points"];SimAttachedHAL.read_depth_clouds+SimSensorBridge._publish_depth_clouds_from_obspublish it as abase_linkPointCloud2. Verified live:--enable-octomapran the full chain —/openral/cameras/front_depth/points(62 k pts,base_link, ~5 Hz, geometrically in front of the base near the ground) →octomap_server(/octomap_binary) →/openral/world_voxels(the kernel's world-collision input). The manualdeproject_depth_imagehelper was superseded by Isaac's convention-correctget_pointcloudand removed. - 2-D lidar → real
/scan(DONE, verified) — the scene casts a PhysXraycast_closestfan (_scan_ranges,n_channelsbeams-π→+πinbase_link, rotated to world by the base yaw) over a few static obstacles it seeds (_add_obstacles, since a bare ground plane returns no hits). Each ray startsrange_min_mbeyond the base so it clears the robot's own chassis/arm (a centre-origin ray hitspanda_link1at distance 0); robot hits (prim under/panda) are ignored. Surfaced asobs["scan"]→SimAttachedHAL.read_scan→SimSensorBridge._compute_scan_ranges. Verified at theSimRolloutlevel (tests/sim/test_panda_mobile_isaac.py: 360 beams, a meaningful fraction hit the obstacles, none NaN). Live in the graphSimSensorBridgepublishes/scan@10 Hz and Nav2's full stack activates (/navigate_to_pose, planner/controller/costmaps/bt_navigator). - Full slam-map + obstacle-aware Nav2 (DONE, verified live 2026-06-11) — the slam loop needs a
common clock (slam + Nav2 run
use_sim_time:true); the deploy-sim/clockpublisher (ADR-0048, #309) on master closes it. With this branch rebased onto that master,openral deploy sim --config scenes/deploy/isaac_panda_mobile_urdf.yaml --enable-sim-clock --enable-slam --enable-nav2ran the complete autonomous-navigation loop on Isaac panda_mobile: the HAL published/clockfrom sim time,slam_toolboxregistered the Isaac lidar and built a 388×480 @ 0.05 m/mapfrom the/scan, Nav2's planner+controller+costmaps came up, and aNavigateToPosegoal to(1.6, 0)inmapreturned SUCCEEDED with/odomadvancing(0,0) → (1.38, 0.01)— Nav2 planned a path, drove the base via/cmd_vel → MobileBaseBridge → BODY_TWIST → _apply_body_twist_via_env_step → Isaac kinematic base, under the active safety kernel. End-to-end: a manifest-driven, URDF-imported robot navigating autonomously around obstacles in Isaac Sim, with every rSkill ROS topic/controller (/joint_states, cameras, depthPointCloud2,/scan,/odom,/map,/cmd_vel) live.
Backward compatibility & safety
The hardcoded lift_cube / bowl_plate layouts stay (selected by --layout); --robot-spec
selects the new generic path, so the verified PoC scenes are untouched. No schema change —
PhysicsBackend.ISAACSIM already exists; the robot spec is an IPC transport detail, never an
on-disk schema. No safety-kernel change — Isaac stays behind SimRollout/SimAttachedHAL;
the real-HW exclusion in build_hal (mode="real" + scene attach → ROSConfigError) still
prevents a sim scene from attaching to physical actuators. No new flag bypasses any safety check.
Open questions
- IPC transport. Reuse the RLDX-1
pyzmq/msgpacksidecar pattern (rldxgroup) vs a bespoke channel — decide at implementation time. Leaningpyzmq/msgpackfor consistency. - Sidecar env provisioning. How the py3.11 Isaac Lab env is created and discovered by the
in-process backend (env var pointing at the sidecar interpreter vs a
uv-managed sibling env) — settle when the first PoC lands.