Skip to content
Merged
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 skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ User skills:
- `user/install-isaac-lab/`: install Isaac Lab following the current install docs — automatic uv setup, downloaded Isaac Sim package, source build, Isaac Lab wheel, legacy isaaclab.sh installer, or Docker — across Linux (x86_64, aarch64) and Windows 11.
- `user/migrate-from-isaac-gym/`: migrate Isaac Gym tasks, assets, and training workflows to Isaac Lab.
- `user/migrate-2x-to-3x/`: migrate Isaac Lab 2.x projects to Isaac Lab 3.0 using the official migration guide.
- `user/domain-randomization-events/`: implement domain randomization through Isaac Lab event terms.
- `user/domain-randomization-events/`: implement fixed and adaptive domain randomization through event and curriculum terms.
- `user/create-environments/`: create manager-based Isaac Lab environments by default, with direct environments for special cases.
- `user/convert-direct-to-manager/`: convert validated direct Isaac Lab environments into manager-based task configurations.
- `user/train-rl-agents/`: configure and run Isaac Lab reinforcement learning workflows.
Expand Down
10 changes: 7 additions & 3 deletions skills/user/domain-randomization-events/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: isaaclab-randomizing-with-events
description: Implements Isaac Lab domain randomization with event terms in direct and manager-based workflows. Use when adding domain randomization, randomizing physics or observations, porting reset randomization, or configuring event-based variation.
description: Implements Isaac Lab domain randomization with event terms and success-driven ADR curricula. Use when randomizing physics or observations, porting reset randomization, or configuring Automatic or Adaptive Domain Randomization (ADR).
audience: user
status: stable
owners:
Expand All @@ -11,7 +11,7 @@ owners:

## When To Use

Use this skill when a user wants to add domain randomization to an Isaac Lab task through event terms.
Use this skill when a user wants to add domain randomization to an Isaac Lab task through event terms. Use ADR when policy success should change the randomization range.

Do not use this skill for unrelated curriculum, command sampling, or reward shaping unless those changes interact with randomization.

Expand All @@ -33,6 +33,7 @@ Do not use this skill for unrelated curriculum, command sampling, or reward shap
10. Use one clear distribution and range for each randomized quantity.
11. Validate with a small number of environments and repeated resets on each backend.
12. Expand ranges only after the baseline randomized task is stable.
13. For manager-based ADR, validate the final range as a fixed event, then use a task-owned scheduler and `modify_term_cfg` to interpolate it; see the [gravity example](examples.md#success-driven-adr).

## Validation

Expand All @@ -43,6 +44,7 @@ Use the plan-validate-execute loop:
3. Check CPU/GPU expectations in the implementation. Some PhysX paths use CPU tensors while Newton paths may operate on the environment device.
4. Run a small reset or rollout smoke test for every targeted backend.
5. Fix shape, device, backend, and entity-name errors before scaling.
6. For ADR, test the exact endpoints and confirm policy success changes the next reset's range within its bounds.

For skill changes, run:

Expand All @@ -52,7 +54,7 @@ uv run --no-project python tools/skills/cli.py check

## Maintenance

Keep this skill synchronized with `source/isaaclab/isaaclab/managers/event_manager.py`, `source/isaaclab/isaaclab/envs/direct_rl_env.py`, `source/isaaclab/isaaclab/envs/direct_marl_env.py`, the direct and manager-based environment tutorials, and the managers API docs. If event-term behavior or mode semantics change, update the official docs or examples first and keep this skill focused on selecting the right workflow.
Keep this skill synchronized with `source/isaaclab/isaaclab/managers/event_manager.py`, `source/isaaclab/isaaclab/envs/direct_rl_env.py`, `source/isaaclab/isaaclab/envs/direct_marl_env.py`, the direct and manager-based environment tutorials, and the managers API docs. For ADR, follow the curriculum guide and Core Lift example.

## References

Expand All @@ -62,4 +64,6 @@ Keep this skill synchronized with `source/isaaclab/isaaclab/managers/event_manag
- [Event manager source](../../../source/isaaclab/isaaclab/managers/event_manager.py)
- [Direct workflow randomization tutorial](../../../docs/source/tutorials/03_envs/create_direct_rl_env.rst)
- [Manager-based event terms tutorial](../../../docs/source/tutorials/03_envs/create_manager_base_env.rst)
- [Curriculum utilities guide](../../../docs/source/how-to/curriculums.rst)
- [Core Lift ADR config](../../../source/isaaclab_tasks/isaaclab_tasks/core/lift/adr_curriculum.py)
- [Managers API](../../../docs/source/api/lab/isaaclab.managers.rst)
15 changes: 15 additions & 0 deletions skills/user/domain-randomization-events/evaluations.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Scenario 3: interval disturbance
- Scenario 4: PhysX and Newton material randomization
- Scenario 5: direct workflow events
- Scenario 6: success-driven ADR

## Scenario 1: Reset Randomization

Expand Down Expand Up @@ -84,3 +85,17 @@ Known failure modes:

- Claims event terms are only for manager-based environments.
- Converts the task to manager-based solely to use randomization events.

## Scenario 6: Success-Driven ADR

Query: "Start gravity at zero and automatically ramp it to -9.81 as my lift policy succeeds."

Expected behavior:

- Keeps gravity application in a reset event.
- Uses a manager-based curriculum to interpolate from zero to full gravity as policy success changes.
- Uses a task-owned scheduler and validates both endpoint values.

Pass/fail criteria:

- Links the curriculum guide and Core Lift ADR example.
33 changes: 33 additions & 0 deletions skills/user/domain-randomization-events/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Reset state randomization
- Direct workflow event config
- Success-driven ADR
- Prestartup USD randomization
- Startup property randomization
- Backend-specific material randomization
Expand Down Expand Up @@ -31,6 +32,38 @@ Expected setup:
- Keep reward and observation logic in direct methods.
- Validate that `prestartup`, `startup`, `reset`, and `interval` modes fire at the expected times for direct workflows.

## Success-Driven ADR

Input: start a manager-based lift task at zero gravity, then approach full gravity as the policy succeeds.

Given a reset event named `variable_gravity`, add a task-owned scheduler and interpolation term:

```python
from isaaclab.managers import CurriculumTermCfg as CurrTerm
from isaaclab.utils.configclass import configclass

from . import mdp


@configclass
class CurriculumCfg:
adr = CurrTerm(func=mdp.DifficultyScheduler, params={"init_difficulty": 0, "min_difficulty": 0, "max_difficulty": 10})
gravity_adr = CurrTerm(
func=mdp.modify_term_cfg,
params={
"address": "events.variable_gravity.params.gravity_distribution_params",
"modify_fn": mdp.initial_final_interpolate_fn,
"modify_params": {
"initial_value": ((0.0, 0.0, 0.0), (0.0, 0.0, 0.0)),
"final_value": ((0.0, 0.0, -9.81), (0.0, 0.0, -9.81)),
"difficulty_term_str": "adr",
},
},
)
```

Assign `curriculum: CurriculumCfg = CurriculumCfg()` on the environment config. Adapt the [Core Lift ADR scheduler](../../../source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/curriculums.py) and test both endpoints.

## Startup Property Randomization

Input: randomize a property once after simulation starts.
Expand Down
Empty file.
9 changes: 5 additions & 4 deletions source/isaaclab/isaaclab/cloner/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@

def _row_env_ids(plan: ClonePlan, row: int) -> tuple[int, ...]:
"""Env ids populated from a plan row: the plan's env ids at the row's ``True`` columns."""
columns = plan.clone_mask[row].nonzero(as_tuple=False).flatten().tolist()
columns = plan.clone_mask[row].nonzero(as_tuple=False).flatten()
if plan.env_ids is None:
return tuple(int(column) for column in columns)
return tuple(int(plan.env_ids[column]) for column in columns)
return tuple(columns.tolist())
columns = columns.to(plan.env_ids.device)
return tuple(plan.env_ids[columns].tolist())


def _column_for_env_id(plan: ClonePlan, env_id: int) -> int | None:
Expand Down Expand Up @@ -82,7 +83,7 @@ def _clone_rows(plan: ClonePlan, path_expr: str, *, populated_only: bool) -> lis
for row, template in enumerate(plan.destinations):
if "{}" not in template:
continue
if populated_only and not _row_env_ids(plan, row):
if populated_only and not plan.clone_mask[row].any():
continue
matched = pth.match(path_expr, template)
if matched is None:
Expand Down
37 changes: 35 additions & 2 deletions source/isaaclab/test/cloner/test_clone_plan_algebra.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,8 @@ def test_iter_sources_yields_nearest_owner():
]


def test_iter_sources_skips_rows_without_envs():
"""A row populating no env is not a source of anything."""
def test_iter_sources_reports_only_the_envs_a_row_populates():
"""A row covering part of the envs is a source for those envs only."""
plan = _plan(
("/World/envs/env_2/Object",),
("/World/envs/env_{}/Object",),
Expand All @@ -352,6 +352,24 @@ def test_iter_sources_skips_rows_without_envs():
]


def test_iter_sources_skips_rows_without_envs():
"""A nearer template populating no env does not hide the populated ancestor owning the path."""
plan = _plan(
("/World/envs/env_0/Robot", "/World/envs/env_0/Robot/wrist/Camera"),
("/World/envs/env_{}/Robot", "/World/envs/env_{}/Robot/wrist/Camera"),
[[True, True, False, False], [False, False, False, False]],
)

assert list(cloner.query.iter_sources(plan, "/World/envs/env_[^/]+/Robot/wrist/Camera")) == [
(
"/World/envs/env_0/Robot",
"/World/envs/env_{}/Robot",
"/World/envs/env_0/Robot/wrist/Camera",
(0, 1),
)
]


def test_iter_sources_distinct_env_root():
"""The destination template need not sit under the default env root."""
plan = PLANS["distinct_env_root"]
Expand Down Expand Up @@ -494,6 +512,21 @@ def test_query_translates_env_ids_through_the_plan():
assert source + suffix == path


@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is unavailable")
def test_query_gathers_env_ids_across_devices():
"""Environment ids can remain on CPU when the clone mask is on CUDA."""
plan = ClonePlan(
sources=("/World/envs/env_2/Robot",),
destinations=("/World/envs/env_{}/Robot",),
clone_mask=torch.tensor([[True, False, True]], dtype=torch.bool, device="cuda"),
env_ids=torch.tensor([2, 5, 8], dtype=torch.long),
)
path = "/World/envs/env_2/Robot/base"

assert cloner.query.path_env_ids(plan, path) == (2, 8)
assert next(iter(cloner.query.iter_sources(plan, "/World/envs/env_[^/]+/Robot")))[3] == (2, 8)


@pytest.mark.parametrize("env_id", [-1, 4, 99])
def test_query_rejects_env_ids_outside_the_plan(env_id):
"""Out-of-range and negative ids resolve to nothing instead of wrapping the mask."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Changed
^^^^^^^

* Extended Core Lift and Reorient task episodes to 12 seconds and their pose-command resampling range to 4--6 seconds.
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class CommandsCfg:
object_pose = mdp.ObjectUniformPoseCommandCfg(
asset_name="robot",
object_name="object",
resampling_time_range=(3.0, 5.0),
resampling_time_range=(4.0, 6.0),
debug_vis=False,
ranges=mdp.ObjectUniformPoseCommandCfg.Ranges(
pos_x=(-0.7, -0.3),
Expand Down Expand Up @@ -566,9 +566,8 @@ def __post_init__(self):
self.decimation = 4 # 30 Hz

# *single-goal setup
self.commands.object_pose.resampling_time_range = (2.0, 3.0)
self.commands.object_pose.position_only = False
self.episode_length_s = 6.0
self.episode_length_s = 12.0
self.is_finite_horizon = False

# simulation settings
Expand Down
60 changes: 56 additions & 4 deletions tools/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,58 @@ def pytest_ignore_collect(collection_path, config):
from the kill.
"""

EXIT_TESTS_FAILED = 1
"""Exit code for a run that completed with at least one failing assertion."""

EXIT_CRASHED = 20
"""Exit code for a run where a test process died without writing its report."""

EXIT_TIMEOUT = 21
"""Exit code for a run where a test reached its hard timeout."""

EXIT_STARTUP_HANG = 22
"""Exit code for a run where a test never finished starting up."""

EXIT_CODE_LABELS = {
0: "all tests passed",
EXIT_TESTS_FAILED: "test failures",
EXIT_CRASHED: "crashed process",
EXIT_TIMEOUT: "timeout",
EXIT_STARTUP_HANG: "startup hang",
}
"""Label for each exit code, printed with the result summary."""


def resolve_exit_code(num_failing: int, num_timeout: int, num_crashed: int, num_startup_hang: int) -> int:
"""Return the exit code for a finished run, based on how it failed.

A crashed process, a hang and a failing assertion have different owners, so
each reports its own code rather than a shared ``1``; the code alone is then
enough to route a red job. When a run hits more than one, the code reports
the outcome that proved the least: a process that died never reached the
assertion a failing test did. ``1`` still means failing assertions, so
callers that only check for a non-zero code are unaffected.

Args:
num_failing: Test files that ran and reported a failing assertion.
num_timeout: Test files killed at their hard timeout.
num_crashed: Test files whose process exited without writing a report.
num_startup_hang: Test files killed before startup finished.

Returns:
``0`` when nothing failed, otherwise the code for the highest-precedence
mode present: crash, then timeout, then startup hang, then assertion.
"""
if num_crashed:
return EXIT_CRASHED
if num_timeout:
return EXIT_TIMEOUT
if num_startup_hang:
return EXIT_STARTUP_HANG
if num_failing:
return EXIT_TESTS_FAILED
return 0


def capture_test_output_with_timeout(cmd, timeout, env, startup_deadline=0, report_file=""):
"""Run a command with timeout and capture all output while streaming in real-time.
Expand Down Expand Up @@ -1543,6 +1595,9 @@ def pytest_sessionstart(session):
summary_str += f"Timeout: {num_timeout}\n"
summary_str += f"Passing Percentage: {passing_percentage:.2f}%\n"

exit_code = resolve_exit_code(num_failing, num_timeout, num_crashed, num_startup_hang)
summary_str += f"Exit Code: {exit_code} ({EXIT_CODE_LABELS[exit_code]})\n"

total_wall = sum(test_status[test_path]["wall_time"] for test_path in test_files)
total_test = sum(test_status[test_path]["time_elapsed"] for test_path in test_files)

Expand All @@ -1559,7 +1614,4 @@ def pytest_sessionstart(session):
logger.info(summary_str)

# Exit pytest after custom execution to prevent normal pytest from overwriting our report
pytest.exit(
"Custom test execution completed",
returncode=0 if (num_failing == 0 and num_timeout == 0 and num_crashed == 0 and num_startup_hang == 0) else 1,
)
pytest.exit("Custom test execution completed", returncode=exit_code)
2 changes: 1 addition & 1 deletion tools/wheel_builder/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ esac
rm -rf "$BUILD_DIR" "$DIST_DIR"
mkdir -p "$BUILD_DIR/src/isaaclab"

# 1. Copy inventory (same as python_packages.toml inventory.includes.all)
# 1. Copy inventory (the full source tree: apps/ + source/)
cp -r apps "$BUILD_DIR/src/isaaclab/"
cp -r source "$BUILD_DIR/src/isaaclab/"

Expand Down
Loading