diff --git a/skills/README.md b/skills/README.md index 8b9e2a4edd29..10ebf6a25ef8 100644 --- a/skills/README.md +++ b/skills/README.md @@ -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. diff --git a/skills/user/domain-randomization-events/SKILL.md b/skills/user/domain-randomization-events/SKILL.md index 2c2054ffda6e..95b4221c83d1 100644 --- a/skills/user/domain-randomization-events/SKILL.md +++ b/skills/user/domain-randomization-events/SKILL.md @@ -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: @@ -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. @@ -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 @@ -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: @@ -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 @@ -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) diff --git a/skills/user/domain-randomization-events/evaluations.md b/skills/user/domain-randomization-events/evaluations.md index 6fe8cf2363aa..4d25334d3d14 100644 --- a/skills/user/domain-randomization-events/evaluations.md +++ b/skills/user/domain-randomization-events/evaluations.md @@ -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 @@ -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. diff --git a/skills/user/domain-randomization-events/examples.md b/skills/user/domain-randomization-events/examples.md index 4010eb0818e1..d9990f5af7f0 100644 --- a/skills/user/domain-randomization-events/examples.md +++ b/skills/user/domain-randomization-events/examples.md @@ -4,6 +4,7 @@ - Reset state randomization - Direct workflow event config +- Success-driven ADR - Prestartup USD randomization - Startup property randomization - Backend-specific material randomization @@ -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. diff --git a/source/isaaclab/changelog.d/clone-query-vectorized.skip b/source/isaaclab/changelog.d/clone-query-vectorized.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab/isaaclab/cloner/query.py b/source/isaaclab/isaaclab/cloner/query.py index e36df47614fa..bbc80f550bda 100644 --- a/source/isaaclab/isaaclab/cloner/query.py +++ b/source/isaaclab/isaaclab/cloner/query.py @@ -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: @@ -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: diff --git a/source/isaaclab/test/cloner/test_clone_plan_algebra.py b/source/isaaclab/test/cloner/test_clone_plan_algebra.py index 1e4f148086af..3794e964dbd9 100644 --- a/source/isaaclab/test/cloner/test_clone_plan_algebra.py +++ b/source/isaaclab/test/cloner/test_clone_plan_algebra.py @@ -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",), @@ -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"] @@ -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.""" diff --git a/source/isaaclab_tasks/changelog.d/zhengyuz-extend-reorient-episode-duration.rst b/source/isaaclab_tasks/changelog.d/zhengyuz-extend-reorient-episode-duration.rst new file mode 100644 index 000000000000..4ae09f0847ee --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/zhengyuz-extend-reorient-episode-duration.rst @@ -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. diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/lift_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/lift_env_cfg.py index 8ce93bf7150f..94b6d2fc44f7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/lift_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/lift_env_cfg.py @@ -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), @@ -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 diff --git a/tools/conftest.py b/tools/conftest.py index c0f810dd0b74..e64b7a5216b1 100644 --- a/tools/conftest.py +++ b/tools/conftest.py @@ -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. @@ -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) @@ -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) diff --git a/tools/wheel_builder/build.sh b/tools/wheel_builder/build.sh index 5930c2634449..bb534b986a57 100755 --- a/tools/wheel_builder/build.sh +++ b/tools/wheel_builder/build.sh @@ -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/"