Skip to content

Commit 0421f0b

Browse files
committed
Merge remote-tracking branch 'upstream/develop' into jichuanh/uv-lock-docker-install
2 parents 30e2e5e + 1301263 commit 0421f0b

6 files changed

Lines changed: 383 additions & 21 deletions

File tree

scripts/tools/test/test_train_and_publish_checkpoints.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77

88
from argparse import Namespace
99
from pathlib import Path
10+
from types import SimpleNamespace
1011

1112
import pytest
1213

14+
from isaaclab_tasks.utils.preset_target import PresetTarget
15+
1316
from scripts.tools.train_and_publish_checkpoints import (
1417
CheckpointJob,
18+
_build_core_jobs,
1519
_play_command,
1620
_select_physics_variants,
1721
_training_command,
@@ -20,6 +24,32 @@
2024
)
2125

2226

27+
def test_build_core_jobs_skips_unsupported_preset_without_normalizing_default(
28+
monkeypatch: pytest.MonkeyPatch,
29+
) -> None:
30+
"""An unsupported preset-only task must not abort construction of the supported core matrix."""
31+
task_spec = SimpleNamespace(
32+
id="Isaac-Unsupported-Core-Task",
33+
kwargs={
34+
"env_cfg_entry_point": "isaaclab_tasks.core.unsupported:UnsupportedEnvCfg",
35+
"rsl_rl_cfg_entry_point": "isaaclab_tasks.core.unsupported:UnsupportedAgentCfg",
36+
},
37+
)
38+
monkeypatch.setattr("scripts.tools.train_and_publish_checkpoints.gym.registry", {task_spec.id: task_spec})
39+
monkeypatch.setattr("scripts.tools.train_and_publish_checkpoints.parse_env_cfg", lambda _: object())
40+
monkeypatch.setattr(
41+
"scripts.tools.train_and_publish_checkpoints.enumerate_task_presets",
42+
lambda _: {PresetTarget.PHYSICS: ["newton_kamino"]},
43+
)
44+
monkeypatch.setattr(
45+
"scripts.tools.train_and_publish_checkpoints.get_pretrained_checkpoint_backend_names",
46+
lambda _: pytest.fail("preset-only tasks must not normalize their unsupported default backend"),
47+
)
48+
args = Namespace(physics_backends="physx,newtonmjwarp", render_backends="rtx,newton")
49+
50+
assert _build_core_jobs(args) == []
51+
52+
2353
def test_job_commands_use_uv_run_isaaclab() -> None:
2454
"""Training and playback must use the uv-managed Isaac Lab CLI."""
2555
job = CheckpointJob(
@@ -49,6 +79,15 @@ def test_select_physics_variants_uses_concrete_isaac_sim_physx() -> None:
4979
assert selections == [("physx", "isaacsim_physx"), ("newtonmjwarp", "newton_mjwarp")]
5080

5181

82+
def test_select_physics_variants_includes_franka_osc_newton_mjwarp() -> None:
83+
"""The effort-limited OSC task is supported by Newton MJWarp."""
84+
selections = _select_physics_variants(
85+
"Isaac-Reach-Franka-OSC", ["isaacsim_physx", "newton_mjwarp"], "physx", ["newtonmjwarp"]
86+
)
87+
88+
assert selections == [("newtonmjwarp", "newton_mjwarp")]
89+
90+
5291
def test_select_physics_variants_does_not_fall_back_to_automatic_physx() -> None:
5392
"""A task without a concrete Isaac Sim selector must not run as OvPhysX."""
5493
selections = _select_physics_variants("Isaac-Test", ["physx", "ovphysx"], "physx", ["physx"])

scripts/tools/train_and_publish_checkpoints.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,6 @@
101101

102102
_TRAINING_COMPLETE_FILENAME = ".pretrained_checkpoint_training_complete"
103103
_CORE_WORKFLOWS = ("rl_games", "rsl_rl", "skrl")
104-
_NEWTON_MJWARP_EXCLUSIONS = {
105-
# The OSC controller destabilizes MJWarp's articulated-body dynamics.
106-
"Isaac-Reach-Franka-OSC",
107-
}
108104

109105

110106
@dataclass(frozen=True)
@@ -255,7 +251,7 @@ def _select_workflow(task_spec: gym.EnvSpec, env_cfg) -> tuple[str, str | None,
255251
def _select_physics_variants(
256252
task_name: str,
257253
variants: list[str],
258-
default_backend: str,
254+
default_backend: str | None,
259255
requested_backends: list[str],
260256
) -> list[tuple[str, str | None]]:
261257
"""Return normalized physics backends and their task preset selectors."""
@@ -270,8 +266,6 @@ def _select_physics_variants(
270266
(candidate for candidate in ("newton_mjwarp", "newton_mjwarp_vbd") if candidate in variants),
271267
None,
272268
)
273-
if task_name in _NEWTON_MJWARP_EXCLUSIONS:
274-
selector = None
275269
if selector is None:
276270
continue
277271
elif backend != default_backend:
@@ -319,12 +313,14 @@ def _build_core_jobs(args: argparse.Namespace) -> list[CheckpointJob]:
319313
if not _is_core_task(task_spec):
320314
continue
321315

322-
env_cfg = parse_env_cfg(task_spec.id)
323-
default_physics, _ = get_pretrained_checkpoint_backend_names(env_cfg)
324-
workflow, agent, algorithm = _select_workflow(task_spec, env_cfg)
325316
preset_map = enumerate_task_presets(task_spec.id) or {}
326317
physics_variants = preset_map.get(PresetTarget.PHYSICS, [])
327318
render_variants = preset_map.get(PresetTarget.RENDERER, [])
319+
env_cfg = parse_env_cfg(task_spec.id)
320+
workflow, agent, algorithm = _select_workflow(task_spec, env_cfg)
321+
default_physics = None
322+
if not physics_variants:
323+
default_physics, _ = get_pretrained_checkpoint_backend_names(env_cfg)
328324

329325
physics_selections = _select_physics_variants(
330326
task_spec.id,

source/isaaclab_rl/changelog.d/maximiliank-skip-unsupported-checkpoint-presets.skip

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed the zero agent to infer finite hold commands for absolute task-space controllers, support composite and
5+
multi-agent action spaces, and reject invalid task configurations before launching the simulator.

source/isaaclab_rl/isaaclab_rl/entrypoints/simple_agents.py

Lines changed: 144 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,23 @@
66
"""Checkpoint-free playback workflows for Isaac Lab environments.
77
88
The zero and random agents are variations of playback that need no trained checkpoint:
9-
the policy either emits constant zero actions or samples uniform random actions.
9+
the policy either infers finite zero or hold actions or samples uniform random actions.
1010
"""
1111

1212
from __future__ import annotations
1313

1414
import argparse
1515
import contextlib
1616
import sys
17-
from typing import Literal
17+
from collections.abc import Callable
18+
from typing import Any, Literal
1819

1920
import gymnasium as gym
2021
import torch
2122

2223
from isaaclab.app import add_launcher_args, launch_simulation
24+
from isaaclab.envs.utils.spaces import sample_space
25+
from isaaclab.utils import math as math_utils
2326

2427
import isaaclab_tasks # noqa: F401
2528
from isaaclab_tasks.utils import (
@@ -46,7 +49,7 @@ def run(argv: list[str] | None = None, *, policy: PolicyName) -> None:
4649
4750
Args:
4851
argv: Command-line arguments excluding the executable name. Reads ``sys.argv`` when omitted.
49-
policy: Action policy to apply, either constant zero actions or uniform random actions.
52+
policy: Action policy to apply, either inferred zero actions or uniform random actions.
5053
5154
Raises:
5255
ValueError: If the requested policy is not supported.
@@ -61,13 +64,18 @@ def run(argv: list[str] | None = None, *, policy: PolicyName) -> None:
6164
# parse configuration via Hydra (supports preset selection, e.g. env.sim.physics=newton_mjwarp)
6265
env_cfg, _ = resolve_task_config(args_cli.task, "")
6366

64-
with launch_simulation(env_cfg, args_cli):
65-
# override with CLI arguments
66-
env_cfg.scene.num_envs = args_cli.num_envs if args_cli.num_envs is not None else env_cfg.scene.num_envs
67-
env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device
68-
if args_cli.disable_fabric:
69-
env_cfg.sim.use_fabric = False
67+
# override with CLI arguments and reject unsupported configurations before
68+
# launching Kit or initializing a native physics backend.
69+
env_cfg.scene.num_envs = args_cli.num_envs if args_cli.num_envs is not None else env_cfg.scene.num_envs
70+
env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device
71+
if args_cli.disable_fabric:
72+
env_cfg.sim.use_fabric = False
73+
try:
74+
env_cfg.validate()
75+
except (TypeError, ValueError) as exc:
76+
raise SystemExit(f"Invalid environment configuration: {exc}") from None
7077

78+
with launch_simulation(env_cfg, args_cli):
7179
# create environment
7280
env = gym.make(args_cli.task, cfg=env_cfg)
7381

@@ -76,11 +84,11 @@ def run(argv: list[str] | None = None, *, policy: PolicyName) -> None:
7684
print(f"[INFO]: Gym action space: {env.action_space}")
7785
# reset environment
7886
env.reset()
87+
zero_action_policy = _create_zero_action_policy(env) if policy == "zero" else None
7988
# simulate environment
8089
# keep running while any visualizer is open, and until the step budget is exhausted
8190
sim = env.unwrapped.sim
8291
device = env.unwrapped.device
83-
zero_actions = torch.zeros(env.action_space.shape, device=device)
8492
step = 0
8593
while sim.is_headless_or_exist_active_visualizer():
8694
if args_cli.max_steps is not None and step >= args_cli.max_steps:
@@ -89,7 +97,7 @@ def run(argv: list[str] | None = None, *, policy: PolicyName) -> None:
8997
# run everything in inference mode
9098
with torch.inference_mode():
9199
if policy == "zero":
92-
actions = zero_actions
100+
actions = zero_action_policy()
93101
else:
94102
# sample actions from -1 to 1
95103
actions = 2 * torch.rand(env.action_space.shape, device=device) - 1
@@ -99,6 +107,131 @@ def run(argv: list[str] | None = None, *, policy: PolicyName) -> None:
99107
env.close()
100108

101109

110+
def _create_zero_action_policy(env: gym.Env) -> Callable[[], Any]:
111+
"""Create a policy that emits finite actions for passive environment playback.
112+
113+
Manager-based environments infer hold commands for absolute task-space action terms and use literal zeros for all
114+
other terms. Direct-workflow environments use zero-filled samples of their declared Gymnasium spaces, including
115+
composite and multi-agent spaces.
116+
"""
117+
unwrapped = env.unwrapped
118+
action_manager = getattr(unwrapped, "action_manager", None)
119+
if action_manager is not None:
120+
return _create_manager_zero_action_policy(action_manager, unwrapped)
121+
122+
if hasattr(unwrapped, "action_spaces"):
123+
actions = {
124+
agent: sample_space(space, unwrapped.device, batch_size=unwrapped.num_envs, fill_value=0)
125+
for agent, space in unwrapped.action_spaces.items()
126+
}
127+
return lambda: actions
128+
129+
actions = sample_space(unwrapped.single_action_space, unwrapped.device, batch_size=unwrapped.num_envs, fill_value=0)
130+
return lambda: actions
131+
132+
133+
def _create_manager_zero_action_policy(action_manager: Any, env: Any) -> Callable[[], torch.Tensor]:
134+
"""Create a zero-action policy from the active action terms."""
135+
actions = torch.zeros_like(action_manager.action)
136+
term_policies = []
137+
index = 0
138+
for term_name in action_manager.active_terms:
139+
term = action_manager.get_term(term_name)
140+
term_policy = _create_action_term_zero_policy(term, env)
141+
if term_policy is not None:
142+
term_policies.append((slice(index, index + term.action_dim), term_policy))
143+
index += term.action_dim
144+
145+
def policy() -> torch.Tensor:
146+
actions.zero_()
147+
for action_slice, term_policy in term_policies:
148+
actions[:, action_slice] = term_policy()
149+
if not torch.isfinite(actions).all():
150+
raise RuntimeError("Zero agent inferred non-finite actions from the current environment state.")
151+
return actions
152+
153+
return policy
154+
155+
156+
def _create_action_term_zero_policy(term: Any, env: Any) -> Callable[[], torch.Tensor] | None:
157+
"""Create the specialized zero-action policy required by an action term."""
158+
term_types = {cls.__name__ for cls in type(term).__mro__}
159+
160+
if "PinkInverseKinematicsAction" in term_types:
161+
controlled_frame_ids, controlled_frame_names = term._asset.find_bodies(
162+
list(term.cfg.target_eef_link_names.values()), preserve_order=True
163+
)
164+
if len(controlled_frame_ids) != len(term.cfg.target_eef_link_names):
165+
raise ValueError(
166+
"Expected one controlled body for every Pink IK target. Resolved "
167+
f"{controlled_frame_names} from {list(term.cfg.target_eef_link_names.values())}."
168+
)
169+
if len(controlled_frame_ids) != term._num_frame_tasks:
170+
raise ValueError(
171+
f"Pink IK has {term._num_frame_tasks} variable frame tasks but "
172+
f"{len(controlled_frame_ids)} controlled bodies were configured."
173+
)
174+
175+
def pink_policy() -> torch.Tensor:
176+
frame_poses = term._asset.data.body_link_pose_w.torch[:, controlled_frame_ids].clone()
177+
frame_poses[..., :3] -= env.scene.env_origins.unsqueeze(1)
178+
hand_joint_positions = term._asset.data.joint_pos.torch[:, term._hand_joint_ids]
179+
return torch.cat((frame_poses.flatten(start_dim=1), hand_joint_positions), dim=-1)
180+
181+
return pink_policy
182+
183+
if "DifferentialInverseKinematicsAction" in term_types and not term.cfg.controller.use_relative_mode:
184+
185+
def differential_ik_policy() -> torch.Tensor:
186+
ee_pos, ee_quat = term._compute_frame_pose()
187+
command = ee_pos if term.cfg.controller.command_type == "position" else torch.cat((ee_pos, ee_quat), dim=-1)
188+
return _unscale_action(command, term._scale)
189+
190+
return differential_ik_policy
191+
192+
if "RMPFlowAction" in term_types and not term.cfg.use_relative_mode:
193+
194+
def rmpflow_policy() -> torch.Tensor:
195+
ee_pos, ee_quat = term._compute_frame_pose()
196+
return _unscale_action(torch.cat((ee_pos, ee_quat), dim=-1), term._scale)
197+
198+
return rmpflow_policy
199+
200+
if "OperationalSpaceControllerAction" in term_types and term._pose_abs_idx is not None:
201+
term_actions = torch.zeros_like(term.raw_actions)
202+
203+
def operational_space_policy() -> torch.Tensor:
204+
term_actions.zero_()
205+
term._compute_ee_pose()
206+
term._compute_task_frame_pose()
207+
if term._task_frame_pose_b is None:
208+
ee_pos_task = term._ee_pose_b[:, :3]
209+
ee_quat_task = term._ee_pose_b[:, 3:7]
210+
else:
211+
ee_pos_task, ee_quat_task = math_utils.subtract_frame_transforms(
212+
term._task_frame_pose_b[:, :3],
213+
term._task_frame_pose_b[:, 3:7],
214+
term._ee_pose_b[:, :3],
215+
term._ee_pose_b[:, 3:7],
216+
)
217+
term_actions[:, term._pose_abs_idx : term._pose_abs_idx + 3] = _unscale_action(
218+
ee_pos_task, term._position_scale
219+
)
220+
term_actions[:, term._pose_abs_idx + 3 : term._pose_abs_idx + 7] = _unscale_action(
221+
ee_quat_task, term._orientation_scale
222+
)
223+
return term_actions
224+
225+
return operational_space_policy
226+
227+
return None
228+
229+
230+
def _unscale_action(command: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
231+
"""Map a processed command back to policy-action coordinates without division by zero."""
232+
return torch.where(scale != 0.0, command / scale, torch.zeros_like(command))
233+
234+
102235
def _parse_args(argv: list[str] | None, policy: PolicyName) -> argparse.Namespace:
103236
"""Parse the command line of a checkpoint-free agent and hand the remainder to Hydra."""
104237
parser = argparse.ArgumentParser(description=_DESCRIPTIONS[policy])

0 commit comments

Comments
 (0)