From 732709a45064f657cc2f74c234eba32085bba008 Mon Sep 17 00:00:00 2001 From: Mustafa Haiderbhai Date: Sun, 20 Sep 2026 00:17:52 -0700 Subject: [PATCH 1/2] Unify isaaclab_tasks coding style and share duplicated MDP terms Apply one consistent convention across the core task packages (module docstrings, section banners, mdp and agents import forms, gym registration layout, sorted relative .pyi stubs, ManagerTermBase constructor order) and move the terms duplicated across tasks (survival_success_rate, terminated_penalty, joint_pos_target_l2, DifficultyScheduler, initial_final_interpolate_fn) into isaaclab.envs.mdp. Share the direct/manager physics, camera and asset presets of the cartpole, Ant and Humanoid tasks through common modules, cache per-step constants in the reorient and lift terms, fix the LiftEnvCfg configclass decorator and the lift ADR noise sign, correct (w, x, y, z) quaternion docstrings, and drop dead test scaffolding and no-op tests. --- .../concepts/sensors/joint_wrench_sensor.rst | 6 +- docs/source/features/hydra.rst | 6 +- docs/source/how-to/create_manager_rl_env.rst | 2 +- .../unify-task-mdp-terms.minor.rst | 10 + .../isaaclab/isaaclab/envs/mdp/__init__.pyi | 16 +- .../isaaclab/isaaclab/envs/mdp/curriculums.py | 87 +++- source/isaaclab/isaaclab/envs/mdp/rewards.py | 39 +- .../changelog.d/unify-task-coding-style.skip | 0 .../test/envs/mdp/test_capture_safety.py | 2 +- .../changelog.d/unify-task-coding-style.rst | 31 ++ .../contrib/assemble_trocar/mdp/rewards.py | 8 +- .../assemble_trocar/mdp/terminations.py | 2 + .../contrib/deploy/mdp/observations.py | 4 +- .../contrib/dr_legs/mdp/__init__.pyi | 2 - .../contrib/dr_legs/mdp/rewards.py | 17 +- .../drone_arl/mdp/commands/commands_cfg.py | 2 + .../contrib/drone_arl/mdp/curriculums.py | 3 +- .../contrib/drone_arl/mdp/rewards.py | 9 +- .../keyboard/mdp/commands/typing_commands.py | 2 +- .../locomanip_pick_place/mdp/actions.py | 2 + .../locomanip_pick_place/mdp/observations.py | 2 + .../mdp/pre_trained_policy_action.py | 2 + .../mdp/pre_trained_policy_action_cfg.py | 2 + .../contrib/navigation/mdp/rewards.py | 2 + .../contrib/nist/mdp/curriculums.py | 2 + .../isaaclab_tasks/contrib/nist/mdp/events.py | 2 + .../contrib/pick_place/mdp/observations.py | 2 + .../pick_place/mdp/pick_place_events.py | 2 + .../contrib/place/mdp/observations.py | 2 + .../contrib/stack/mdp/franka_stack_events.py | 7 +- .../contrib/stack/mdp/observations.py | 2 + .../contrib/stack/mdp/stack_events.py | 12 +- .../core/cabinet/cabinet_direct_env.py | 78 ++-- .../core/cabinet/cabinet_direct_env_cfg.py | 2 + .../core/cabinet/cabinet_env_cfg.py | 28 +- .../core/cabinet/config/franka/__init__.py | 34 +- .../config/franka/cabinet_direct_env_cfg.py | 2 + .../config/franka/joint_pos_env_cfg.py | 5 +- .../core/cabinet/mdp/__init__.py | 2 +- .../core/cabinet/mdp/__init__.pyi | 5 +- .../core/cabinet/mdp/observations.py | 2 + .../core/cabinet/mdp/rewards.py | 2 + .../isaaclab_tasks/core/cartpole/__init__.py | 9 +- .../core/cartpole/cartpole_common.py | 88 ++++ .../cartpole/cartpole_direct_camera_env.py | 27 +- .../cartpole_direct_camera_env_cfg.py | 41 +- .../core/cartpole/cartpole_direct_env.py | 12 +- .../core/cartpole/cartpole_direct_env_cfg.py | 44 +- .../cartpole_manager_camera_env_cfg.py | 53 +-- .../core/cartpole/cartpole_manager_env_cfg.py | 62 +-- .../core/cartpole/mdp/__init__.py | 2 +- .../core/cartpole/mdp/__init__.pyi | 10 +- .../core/cartpole/mdp/observations.py | 2 + .../core/cartpole/mdp/rewards.py | 46 -- .../core/fourbar_pole/mdp/__init__.py | 2 +- .../core/fourbar_pole/mdp/__init__.pyi | 14 +- .../core/fourbar_pole/mdp/observations.py | 34 ++ .../core/fourbar_pole/mdp/rewards.py | 34 +- .../isaaclab_tasks/core/handover/__init__.py | 31 +- .../core/handover/handover_env.py | 33 +- .../core/handover/handover_env_cfg.py | 27 +- .../core/handover/handover_manager_env_cfg.py | 102 ++-- .../core/handover/mdp/__init__.py | 2 +- .../core/handover/mdp/commands.py | 10 +- .../isaaclab_tasks/core/lift/__init__.py | 2 - .../core/lift/adr_curriculum.py | 36 +- .../core/lift/config/franka/franka_env_cfg.py | 80 +++- .../core/lift/config/franka_soft/__init__.py | 9 +- .../franka_soft/franka_cable_env_cfg.py | 78 ++-- .../franka_soft/franka_cloth_env_cfg.py | 62 +-- .../config/franka_soft/franka_soft_env_cfg.py | 114 +++-- .../lift/config/kuka_allegro/camera_cfg.py | 29 +- .../kuka_allegro_camera_env_cfg.py | 15 +- .../kuka_allegro/kuka_allegro_env_cfg.py | 49 +- .../isaaclab_tasks/core/lift/lift_env_cfg.py | 91 ++-- .../isaaclab_tasks/core/lift/mdp/__init__.py | 2 + .../isaaclab_tasks/core/lift/mdp/__init__.pyi | 85 ++-- .../core/lift/mdp/commands/__init__.py | 2 + .../core/lift/mdp/commands/__init__.pyi | 6 +- .../core/lift/mdp/commands/pose_commands.py | 43 +- .../lift/mdp/commands/pose_commands_cfg.py | 10 +- .../core/lift/mdp/curriculums.py | 75 +-- .../isaaclab_tasks/core/lift/mdp/events.py | 4 +- .../core/lift/mdp/events_cfg.py | 4 +- .../core/lift/mdp/observations.py | 195 +++----- .../isaaclab_tasks/core/lift/mdp/rewards.py | 129 +++--- .../core/lift/mdp/terminations.py | 71 ++- .../isaaclab_tasks/core/lift/mdp/utils.py | 71 ++- .../core/locomotion/__init__.py | 4 +- .../core/locomotion/ant/__init__.py | 12 +- .../core/locomotion/ant/ant_common.py | 67 +++ .../core/locomotion/ant/ant_direct_env.py | 2 + .../core/locomotion/ant/ant_direct_env_cfg.py | 74 +-- .../locomotion/ant/ant_manager_env_cfg.py | 97 ++-- .../core/locomotion/humanoid/__init__.py | 13 +- .../locomotion/humanoid/humanoid_common.py | 59 +++ .../humanoid/humanoid_direct_env.py | 2 + .../humanoid/humanoid_direct_env_cfg.py | 76 +-- .../humanoid/humanoid_manager_env_cfg.py | 80 +--- .../core/locomotion/locomotion_direct_env.py | 13 +- .../core/locomotion/mdp/__init__.py | 2 +- .../core/locomotion/mdp/__init__.pyi | 14 +- .../core/locomotion/mdp/observations.py | 2 + .../core/locomotion/mdp/rewards.py | 83 ++-- .../isaaclab_tasks/core/pendulum/__init__.py | 2 +- .../core/reach/config/franka/__init__.py | 12 +- .../config/franka/franka_reach_env_cfg.py | 12 +- .../config/franka/franka_reach_osc_env_cfg.py | 25 +- .../core/reach/config/ur_10/__init__.py | 4 +- .../reach/config/ur_10/joint_pos_env_cfg.py | 13 +- .../core/reach/reach_env_cfg.py | 15 +- .../isaaclab_tasks/core/reorient/__init__.py | 1 - .../reorient/config/allegro_hand/__init__.py | 4 +- .../allegro_hand/allegro_hand_common.py | 24 +- .../allegro_hand_direct_env_cfg.py | 6 +- .../allegro_hand_manager_env_cfg.py | 1 + .../reorient/config/shadow_hand/__init__.py | 20 +- .../config/shadow_hand/feature_extractor.py | 43 +- .../shadow_hand_camera_manager_env_cfg.py | 15 +- .../config/shadow_hand/shadow_hand_common.py | 48 +- .../shadow_hand_direct_camera_env.py | 28 +- .../shadow_hand_direct_camera_env_cfg.py | 4 + .../shadow_hand/shadow_hand_direct_env_cfg.py | 13 +- .../shadow_hand_manager_env_cfg.py | 12 +- .../core/reorient/mdp/__init__.py | 2 +- .../core/reorient/mdp/commands.py | 17 +- .../core/reorient/mdp/events.py | 2 +- .../core/reorient/mdp/observations.py | 59 ++- .../core/reorient/mdp/rewards.py | 4 +- .../core/reorient/mdp/terminations.py | 2 +- .../core/reorient/reorient_direct_env.py | 181 ++++---- .../core/reorient/reorient_manager_env_cfg.py | 24 +- .../isaaclab_tasks/core/reorient/utils.py | 8 +- .../core/velocity/config/anymal_d/__init__.py | 2 + .../velocity/config/anymal_d/flat_env_cfg.py | 4 + .../velocity/config/anymal_d/rough_env_cfg.py | 8 +- .../core/velocity/config/cassie/__init__.py | 2 + .../velocity/config/cassie/flat_env_cfg.py | 4 + .../velocity/config/cassie/rough_env_cfg.py | 12 +- .../core/velocity/config/g1/__init__.py | 19 +- .../core/velocity/config/g1/flat_env_cfg.py | 4 + .../core/velocity/config/g1/rough_env_cfg.py | 8 +- .../core/velocity/config/go2/__init__.py | 2 + .../core/velocity/config/go2/flat_env_cfg.py | 4 + .../core/velocity/config/go2/rough_env_cfg.py | 8 +- .../core/velocity/config/h1/__init__.py | 19 +- .../core/velocity/config/h1/flat_env_cfg.py | 4 + .../core/velocity/config/h1/rough_env_cfg.py | 8 +- .../core/velocity/mdp/__init__.py | 2 +- .../core/velocity/mdp/__init__.pyi | 5 +- .../core/velocity/mdp/curriculums.py | 6 +- .../core/velocity/mdp/rewards.py | 41 +- .../core/velocity/mdp/symmetry/anymal.py | 1 - .../core/velocity/mdp/terminations.py | 6 +- .../core/velocity/velocity_env_cfg.py | 22 +- .../test/benchmarking/configs.yaml | 436 ------------------ .../test/benchmarking/conftest.py | 130 ------ .../benchmarking/env_benchmark_test_utils.py | 274 ----------- .../test/core/test_environment_determinism.py | 22 - .../core/test_environments_isaacsim_physx.py | 12 +- .../test/core/test_environments_newton.py | 10 +- .../test/core/test_environments_ovphysx.py | 10 +- .../test/core/test_lazy_export_stubs.py | 46 +- .../test/core/test_lift_env_cfg.py | 41 +- .../core/test_multi_agent_environments.py | 7 +- .../test_newton_actuator_manager_authoring.py | 75 ++- .../test/core/test_video_recording.py | 21 +- source/isaaclab_tasks/test/env_test_utils.py | 28 +- .../test/test_maybe_save_stage_golden.py | 7 - .../changelog.d/unify-task-coding-style.skip | 0 .../core/cartpole/mdp/rewards.py | 2 +- .../core/locomotion/mdp/rewards.py | 4 +- 172 files changed, 2053 insertions(+), 2860 deletions(-) create mode 100644 source/isaaclab/changelog.d/unify-task-mdp-terms.minor.rst create mode 100644 source/isaaclab_experimental/changelog.d/unify-task-coding-style.skip create mode 100644 source/isaaclab_tasks/changelog.d/unify-task-coding-style.rst create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_common.py delete mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/rewards.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/observations.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_common.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_common.py delete mode 100644 source/isaaclab_tasks/test/benchmarking/configs.yaml delete mode 100644 source/isaaclab_tasks/test/benchmarking/conftest.py delete mode 100644 source/isaaclab_tasks/test/benchmarking/env_benchmark_test_utils.py create mode 100644 source/isaaclab_tasks_experimental/changelog.d/unify-task-coding-style.skip diff --git a/docs/source/concepts/sensors/joint_wrench_sensor.rst b/docs/source/concepts/sensors/joint_wrench_sensor.rst index 9b2c84511c81..57a20c185568 100644 --- a/docs/source/concepts/sensors/joint_wrench_sensor.rst +++ b/docs/source/concepts/sensors/joint_wrench_sensor.rst @@ -33,7 +33,8 @@ the same number or order of entries: .. literalinclude:: ../../../../source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_manager_env_cfg.py :language: python - :lines: 78-82 + :start-at: joint_wrench = JointWrenchSensorCfg + :end-at: joint_wrench = JointWrenchSensorCfg Manager-based environments can select a body subset through :class:`~isaaclab.managers.SceneEntityCfg` and use @@ -41,7 +42,8 @@ Manager-based environments can select a body subset through .. literalinclude:: ../../../../source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_manager_env_cfg.py :language: python - :lines: 122-131 + :start-at: feet_body_forces = ObsTerm( + :end-at: actions = ObsTerm(func=mdp.last_action) Read the data ------------- diff --git a/docs/source/features/hydra.rst b/docs/source/features/hydra.rst index 3359b57b9aaa..ed07fbaa7d50 100644 --- a/docs/source/features/hydra.rst +++ b/docs/source/features/hydra.rst @@ -150,7 +150,7 @@ For example, for the configuration of the Cartpole camera environment: .. literalinclude:: ../../../source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py :language: python - :start-at: class CartpoleTiledCameraCfg + :start-at: class CartpoleCameraEnvCfg(PresetCfg): :end-at: observation_space = [3, 96, 96] The configuration declares the single-frame channel count and a default spatial size. @@ -313,10 +313,10 @@ Physics backend selection uses the same preset system. A task can define a The Cartpole task's definition is a maintained example: -.. literalinclude:: ../../../source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_env_cfg.py +.. literalinclude:: ../../../source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_common.py :language: python :start-at: class CartpolePhysicsCfg(PresetCfg): - :end-before: ## + :end-before: @configclass The ``newton_mjwarp`` and ``newton_kamino`` entries both select the Newton physics backend because both entries are :class:`~isaaclab_newton.physics.NewtonCfg` objects. The difference diff --git a/docs/source/how-to/create_manager_rl_env.rst b/docs/source/how-to/create_manager_rl_env.rst index fe0ec521c6b8..ee489da6d71d 100644 --- a/docs/source/how-to/create_manager_rl_env.rst +++ b/docs/source/how-to/create_manager_rl_env.rst @@ -38,7 +38,7 @@ For this tutorial, we use the cartpole environment defined in ``isaaclab_tasks.c .. literalinclude:: ../../../source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_env_cfg.py :language: python - :emphasize-lines: 117-141, 144-154, 172-174 + :emphasize-lines: 67-84, 87-110, 174-184 :linenos: The script for running the environment ``run_cartpole_rl_env.py`` is present in the diff --git a/source/isaaclab/changelog.d/unify-task-mdp-terms.minor.rst b/source/isaaclab/changelog.d/unify-task-mdp-terms.minor.rst new file mode 100644 index 000000000000..b3b12892296e --- /dev/null +++ b/source/isaaclab/changelog.d/unify-task-mdp-terms.minor.rst @@ -0,0 +1,10 @@ +Added +^^^^^ + +* Added :class:`~isaaclab.envs.mdp.rewards.survival_success_rate`, :func:`~isaaclab.envs.mdp.rewards.terminated_penalty` + and :func:`~isaaclab.envs.mdp.rewards.joint_pos_target_l2` reward terms, previously duplicated across the cartpole, + locomotion and DR-legs task packages. +* Added :class:`~isaaclab.envs.mdp.curriculums.DifficultyScheduler` and + :func:`~isaaclab.envs.mdp.curriculums.initial_final_interpolate_fn` for adaptive domain randomization curricula, + previously local to the lift task package. The scheduler reads the success flag from the reward term named by the new + ``success_term_name`` parameter (default ``"success"``). diff --git a/source/isaaclab/isaaclab/envs/mdp/__init__.pyi b/source/isaaclab/isaaclab/envs/mdp/__init__.pyi index ebf54b96ae7a..b01a0af2a01c 100644 --- a/source/isaaclab/isaaclab/envs/mdp/__init__.pyi +++ b/source/isaaclab/isaaclab/envs/mdp/__init__.pyi @@ -46,6 +46,8 @@ __all__ = [ "UniformPoseCommandCfg", "UniformVelocityCommand", "UniformVelocityCommandCfg", + "DifficultyScheduler", + "initial_final_interpolate_fn", "modify_env_param", "modify_reward_weight", "modify_term_cfg", @@ -127,6 +129,7 @@ __all__ = [ "joint_acc_l2", "joint_deviation_l1", "joint_pos_limits", + "joint_pos_target_l2", "joint_torques_l2", "joint_vel_l1", "joint_vel_l2", @@ -135,6 +138,8 @@ __all__ = [ "orientation_command_error", "position_command_error", "position_command_error_tanh", + "survival_success_rate", + "terminated_penalty", "track_ang_vel_z_exp", "track_lin_vel_xy_exp", "undesired_contacts", @@ -197,7 +202,13 @@ from .commands import ( UniformVelocityCommand, UniformVelocityCommandCfg, ) -from .curriculums import modify_env_param, modify_reward_weight, modify_term_cfg +from .curriculums import ( + DifficultyScheduler, + initial_final_interpolate_fn, + modify_env_param, + modify_reward_weight, + modify_term_cfg, +) from .events import ( apply_external_force_torque, push_by_setting_velocity, @@ -282,6 +293,7 @@ from .rewards import ( joint_acc_l2, joint_deviation_l1, joint_pos_limits, + joint_pos_target_l2, joint_torques_l2, joint_vel_l1, joint_vel_l2, @@ -290,6 +302,8 @@ from .rewards import ( orientation_command_error, position_command_error, position_command_error_tanh, + survival_success_rate, + terminated_penalty, track_ang_vel_z_exp, track_lin_vel_xy_exp, undesired_contacts, diff --git a/source/isaaclab/isaaclab/envs/mdp/curriculums.py b/source/isaaclab/isaaclab/envs/mdp/curriculums.py index 8438e5bec925..39694fe38d14 100644 --- a/source/isaaclab/isaaclab/envs/mdp/curriculums.py +++ b/source/isaaclab/isaaclab/envs/mdp/curriculums.py @@ -13,7 +13,9 @@ import re from collections.abc import Sequence -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar + +import torch from isaaclab.managers import CurriculumTermCfg, ManagerTermBase @@ -294,3 +296,86 @@ def __init__(self, cfg, env): super().__init__(cfg, env) # overwrite the simplified address with the full manager path self._address = self._address.replace("s.", "_manager.cfg.", 1) + + +class DifficultyScheduler(ManagerTermBase): + """Adaptive difficulty scheduler for curriculum learning. + + Each environment keeps an integer difficulty level. At episode end the level is promoted when the + reward term named by ``success_term_name`` reports success for that environment through a sticky + boolean ``succeeded`` buffer, and demoted otherwise unless ``promotion_only`` is set. The normalized + mean difficulty across environments is exposed as :attr:`difficulty_frac` for other curriculum terms, + such as :func:`initial_final_interpolate_fn`, to interpolate their targets. + """ + + def __init__(self, cfg: CurriculumTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + init_difficulty: int = cfg.params.get("init_difficulty", 0) + self.current_difficulties = torch.full((env.num_envs,), float(init_difficulty), device=env.device) + self.difficulty_frac: float = 0.0 + """Mean difficulty across environments, normalized by ``max_difficulty``.""" + + def get_state(self) -> torch.Tensor: + return self.current_difficulties + + def set_state(self, state: torch.Tensor) -> None: + self.current_difficulties = state.clone().to(self._env.device) + + def __call__( + self, + env: ManagerBasedRLEnv, + env_ids: Sequence[int], + init_difficulty: int = 0, + min_difficulty: int = 0, + max_difficulty: int = 50, + promotion_only: bool = False, + success_term_name: str = "success", + ) -> float: + succeeded = env.reward_manager.get_term_cfg(success_term_name).func.succeeded[env_ids] + current = self.current_difficulties[env_ids] + demoted = current if promotion_only else current - 1 + self.current_difficulties[env_ids] = torch.where(succeeded, current + 1, demoted).clamp( + min=min_difficulty, max=max_difficulty + ) + # Python float: the dependent curriculum terms compare and interpolate host-side + self.difficulty_frac = (torch.mean(self.current_difficulties) / max(max_difficulty, 1)).item() + return self.difficulty_frac + + +def initial_final_interpolate_fn( + env: ManagerBasedRLEnv, + env_ids: Sequence[int], + data: Any, + initial_value: Any, + final_value: Any, + difficulty_term_str: str, +) -> Any: + """Interpolate a term parameter between initial and final values by the current difficulty fraction. + + Intended as the ``modify_fn`` of :class:`modify_term_cfg`. Works on arbitrarily nested lists and + tuples; scalars (int and float) are interpolated at the leaves and integers stay integers. + + Args: + env: The environment. + env_ids: Environments being updated. Unused, the interpolation is shared by all environments. + data: Current value of the parameter, which fixes the structure and leaf types of the result. + initial_value: Value at zero difficulty. + final_value: Value at maximum difficulty. + difficulty_term_str: Name of the :class:`DifficultyScheduler` curriculum term to read. + + Returns: + The interpolated value, or :attr:`modify_env_param.NO_CHANGE` while the difficulty is below 10%. + """ + difficulty_term: DifficultyScheduler = getattr(env.curriculum_manager.cfg, difficulty_term_str).func + frac = difficulty_term.difficulty_frac + if frac < 0.1: + return modify_env_param.NO_CHANGE + return _interpolate_nested(initial_value, final_value, data, frac) + + +def _interpolate_nested(initial: Any, final: Any, data: Any, frac: float) -> Any: + """Interpolate leaf scalars of nested sequences, preserving the container and leaf types of ``data``.""" + if isinstance(data, Sequence) and not isinstance(data, (str, bytes)): + return type(data)(_interpolate_nested(i, f, d, frac) for i, f, d in zip(initial, final, data)) + value = frac * (final - initial) + initial + return int(value) if isinstance(data, int) else value diff --git a/source/isaaclab/isaaclab/envs/mdp/rewards.py b/source/isaaclab/isaaclab/envs/mdp/rewards.py index 8a673b6e8ef7..3f871380832d 100644 --- a/source/isaaclab/isaaclab/envs/mdp/rewards.py +++ b/source/isaaclab/isaaclab/envs/mdp/rewards.py @@ -18,7 +18,7 @@ from isaaclab.managers import SceneEntityCfg from isaaclab.managers.manager_base import ManagerTermBase from isaaclab.managers.manager_term_cfg import RewardTermCfg -from isaaclab.utils.math import combine_frame_transforms, quat_error_magnitude, quat_mul +from isaaclab.utils.math import combine_frame_transforms, quat_error_magnitude, quat_mul, wrap_to_pi if TYPE_CHECKING: from isaaclab.assets import Articulation, RigidObject @@ -70,6 +70,32 @@ def __call__(self, env: ManagerBasedRLEnv, term_keys: str | list[str] = ".*") -> return (reset_buf * (~env.termination_manager.time_outs)).float() +def terminated_penalty(env: ManagerBasedRLEnv) -> torch.Tensor: + """Penalize early termination once, independently of the environment step size. + + :class:`~isaaclab.managers.RewardManager` scales every term by the step interval, which would make a + plain terminal penalty depend on ``sim.dt`` and ``decimation``. Dividing by the step interval here + cancels that scaling, so the term contributes exactly its weight on the step the episode terminates. + """ + return env.termination_manager.terminated.float() / env.step_dt + + +class survival_success_rate(ManagerTermBase): + """Track episode survival as the success metric. + + The term returns zero reward and only tracks the metric. On episode reset it writes + ``Metrics/success_rate`` into ``extras["log"]``, where an episode counts as a success when it + timed out without terminating early. + """ + + def reset(self, env_ids: torch.Tensor) -> None: + survived = self._env.termination_manager.time_outs[env_ids] + self._env.extras.setdefault("log", {})["Metrics/success_rate"] = survived.float().mean().item() + + def __call__(self, env: ManagerBasedRLEnv) -> torch.Tensor: + return torch.zeros(env.num_envs, device=env.device) + + """ Root penalties. """ @@ -189,6 +215,17 @@ def joint_deviation_l1(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = Scene return torch.sum(torch.abs(angle), dim=1) +def joint_pos_target_l2(env: ManagerBasedRLEnv, target: float, asset_cfg: SceneEntityCfg) -> torch.Tensor: + """Penalize joint positions that deviate from a target value using an L2 squared kernel. + + The joint positions are wrapped to ``[-pi, pi]`` before the deviation is computed. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + joint_pos = wrap_to_pi(asset.data.joint_pos.torch[:, asset_cfg.joint_ids]) + return torch.sum(torch.square(joint_pos - target), dim=1) + + def joint_pos_limits(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Penalize joint positions if they cross the soft limits. diff --git a/source/isaaclab_experimental/changelog.d/unify-task-coding-style.skip b/source/isaaclab_experimental/changelog.d/unify-task-coding-style.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab_experimental/test/envs/mdp/test_capture_safety.py b/source/isaaclab_experimental/test/envs/mdp/test_capture_safety.py index 1bd0d1cfae35..c66f2e913a07 100644 --- a/source/isaaclab_experimental/test/envs/mdp/test_capture_safety.py +++ b/source/isaaclab_experimental/test/envs/mdp/test_capture_safety.py @@ -394,7 +394,7 @@ def _discover_warp_mdp_terms() -> set[str]: """Return every public warp MDP term as a ``":"`` identity. Qualified rather than bare: the same term name legitimately appears in more than one task - mirror (``survival_success_rate`` is defined by both cartpole and locomotion), and keying + mirror (``survival_success_rate`` is twinned by both cartpole and locomotion), and keying by name alone would let a spec for one of them mark the other as declared. """ terms: set[str] = set() diff --git a/source/isaaclab_tasks/changelog.d/unify-task-coding-style.rst b/source/isaaclab_tasks/changelog.d/unify-task-coding-style.rst new file mode 100644 index 000000000000..2c664e7f76e6 --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/unify-task-coding-style.rst @@ -0,0 +1,31 @@ +Changed +^^^^^^^ + +* Unified the coding style of the core task packages: module docstrings, section banners, import style, gym + registration layout, stub (``.pyi``) layout and ``ManagerTermBase`` constructor signatures now follow one convention. +* Moved the duplicated ``survival_success_rate``, ``terminated_penalty``, ``joint_pos_target_l2``, + ``DifficultyScheduler`` and ``initial_final_interpolate_fn`` terms to :mod:`isaaclab.envs.mdp`. The task ``mdp`` + packages keep exposing them through their :mod:`isaaclab.envs.mdp` fallback, so ``mdp.`` references in task + configurations keep working. Import them from :mod:`isaaclab.envs.mdp` instead of the task packages. +* Moved the fourbar-pole ``joint_pos_cos`` and ``joint_pos_sin`` observation terms from ``mdp/rewards.py`` to + ``mdp/observations.py``; they remain available as ``mdp.joint_pos_cos`` and ``mdp.joint_pos_sin``. +* Shared the physics, camera and asset presets of the direct and manager-based cartpole, Ant and Humanoid tasks + through new ``cartpole_common``, ``ant_common`` and ``humanoid_common`` modules instead of duplicating them. +* Renamed the private ``_FrankaSoftSceneCfg`` and ``_FrankaSoftCameraSceneCfg`` scene configurations of the Franka + soft-body tasks to the public ``FrankaSoftBaseSceneCfg`` and ``FrankaSoftBaseCameraSceneCfg``. +* Replaced the deprecated ``viewer`` settings of the handover and Franka soft-body tasks with + ``sim.default_visualizer_cfg``. +* Registered a default agent for the ``Isaac-Shadow-Handover``, ``Isaac-Lift-Cable-Franka`` and + ``Isaac-Lift-Cable-Franka-Camera`` tasks. + +Fixed +^^^^^ + +* Fixed the lift ADR curriculum interpolating the point-cloud noise upper bound towards ``-0.01`` instead of ``0.01``. +* Fixed the ``LiftEnvCfg`` configuration class missing the ``@configclass`` decorator. +* Fixed the in-hand reorientation keypoint helpers rebuilding constant corner offsets on the device every step, and + the lift deformable and cable out-of-bounds terminations allocating constant bound tensors every step. +* Fixed the lift, handover and reorientation tasks rebuilding per-step index and origin tensors with ``repeat`` where a + broadcast suffices. +* Fixed docstrings stating a ``(w, x, y, z)`` quaternion order in the lift, handover, deploy and keyboard task + packages; Isaac Lab uses ``(x, y, z, w)``. diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/rewards.py index c0cb834ed1cc..4abefb912615 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/rewards.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Reward terms for the trocar assembly environment.""" + from __future__ import annotations import logging @@ -11,6 +13,7 @@ import torch +import isaaclab.utils.math as math_utils from isaaclab.managers import SceneEntityCfg from isaaclab.utils.math import quat_apply @@ -295,9 +298,8 @@ def get_trocar_tip_position( Returns: torch.Tensor: Shape (num_envs, 3) - Position in world coordinates """ - from pxr import Gf, Usd, UsdGeom - - import isaaclab.utils.math as math_utils + # USD is a runtime dependency that must not load at config-import time + from pxr import Gf, Usd, UsdGeom # noqa: PLC0415 # Cache the tip offset to avoid recalculating every step. # The local offset from root to tip is a static geometric property of the USD diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/terminations.py index c07540e9bc6f..dc93f9efb226 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/terminations.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Termination terms for the trocar assembly environment.""" + from __future__ import annotations import logging diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/mdp/observations.py index ac12d8b22f7f..0f70d6769c8f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/mdp/observations.py @@ -150,7 +150,7 @@ class gear_shaft_quat_w(ManagerTermBase): asset_cfg: The asset configuration for the gear base. Defaults to SceneEntityCfg("factory_gear_base"). Returns: - Gear shaft orientation tensor as a quaternion (w, x, y, z) with shape (num_envs, 4). + Gear shaft orientation tensor as a quaternion (x, y, z, w) with shape (num_envs, 4). """ def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): @@ -270,7 +270,7 @@ class gear_quat_w(ManagerTermBase): the w component is positive, reducing observation variation for the policy. Returns: - Gear orientation tensor as a quaternion (w, x, y, z) with shape (num_envs, 4). + Gear orientation tensor as a quaternion (x, y, z, w) with shape (num_envs, 4). Raises: RuntimeError: If the gear type manager is not initialized in the environment. diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/__init__.pyi index 594becfd8b49..530a83615766 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/__init__.pyi @@ -19,7 +19,6 @@ __all__ = [ "joint_pd_command_l2", "joint_pos_tracking_exp", "root_orientation_exp", - "survival_success_rate", "walk_success_rate", ] @@ -36,7 +35,6 @@ from .rewards import ( joint_pd_command_l2, joint_pos_tracking_exp, root_orientation_exp, - survival_success_rate, walk_success_rate, ) from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/rewards.py index ce2759adc9bd..135e1a81421b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/rewards.py @@ -37,7 +37,6 @@ "feet_flat", "feet_touchdown_vel", "root_orientation_exp", - "survival_success_rate", "walk_success_rate", ] @@ -226,24 +225,10 @@ def root_orientation_exp( return _exp_se(torch.sum(torch.square(tilt[:, :3]), dim=1), sigma) -class survival_success_rate(ManagerTermBase): - """Logs ``Metrics/success_rate`` = fraction of environments that survived the full episode.""" - - def __init__(self, env: ManagerBasedRLEnv, cfg: RewardTermCfg): - super().__init__(cfg, env) - - def reset(self, env_ids: torch.Tensor): - survived = self._env.termination_manager.time_outs[env_ids] - self._env.extras.setdefault("log", {})["Metrics/success_rate"] = survived.float().mean().item() - - def __call__(self, env: ManagerBasedRLEnv) -> torch.Tensor: - return torch.zeros(env.num_envs, device=env.device) - - class walk_success_rate(ManagerTermBase): """Episode-mean velocity-tracking + gait-contact success metric for the walk task.""" - def __init__(self, env: ManagerBasedRLEnv, cfg: RewardTermCfg): + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): super().__init__(cfg, env) self._err_xy_sum = torch.zeros(env.num_envs, device=env.device) self._err_yaw_sum = torch.zeros(env.num_envs, device=env.device) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/mdp/commands/commands_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/mdp/commands/commands_cfg.py index cbcb4577308c..09fc6a3ca634 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/mdp/commands/commands_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/mdp/commands/commands_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the drone pose command.""" + from typing import TYPE_CHECKING from isaaclab.envs.mdp.commands.commands_cfg import UniformPoseCommandCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/mdp/curriculums.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/mdp/curriculums.py index 3fc33a9328b5..693c230e6005 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/mdp/curriculums.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/mdp/curriculums.py @@ -15,7 +15,6 @@ from typing import TYPE_CHECKING import torch -import warp as wp from isaaclab.managers import ManagerTermBase, SceneEntityCfg from isaaclab.managers.manager_term_cfg import CurriculumTermCfg @@ -92,7 +91,7 @@ def __call__( command = env.command_manager.get_command(command_name) target_position_w = command[:, :3].clone() - current_position = wp.to_torch(asset.data.root_pos_w) - env.scene.env_origins + current_position = asset.data.root_pos_w.torch - env.scene.env_origins position_error = torch.norm(target_position_w[env_ids] - current_position[env_ids], dim=1) # Decide difficulty changes diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/mdp/rewards.py index a691df9e2d1e..9baca143c891 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/drone_arl/mdp/rewards.py @@ -3,12 +3,13 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Reward terms for the drone environments.""" + from __future__ import annotations from typing import TYPE_CHECKING import torch -import warp as wp import isaaclab.utils.math as math_utils from isaaclab.managers import SceneEntityCfg @@ -96,7 +97,7 @@ def distance_to_goal_exp_curriculum( asset: RigidObject = env.scene[asset_cfg.name] command = env.command_manager.get_command(command_name) - current_position = wp.to_torch(asset.data.root_pos_w) - env.scene.env_origins + current_position = asset.data.root_pos_w.torch - env.scene.env_origins # compute the error position_error_square = torch.sum(torch.square(command[:, :3] - current_position), dim=1) @@ -178,11 +179,11 @@ def velocity_to_goal_reward_curriculum( # get the center of the environment command = env.command_manager.get_command(command_name) - current_position = wp.to_torch(asset.data.root_pos_w) - env.scene.env_origins + current_position = asset.data.root_pos_w.torch - env.scene.env_origins direction_to_goal = command[:, :3] - current_position direction_to_goal = direction_to_goal / (torch.norm(direction_to_goal, dim=1, keepdim=True) + 1e-8) # compute the reward as the dot product between the velocity and the direction to the goal - velocity_towards_goal = torch.sum(wp.to_torch(asset.data.root_lin_vel_w) * direction_to_goal, dim=1) + velocity_towards_goal = torch.sum(asset.data.root_lin_vel_w.torch * direction_to_goal, dim=1) # Get curriculum term and compute weight curriculum_term = get_obstacle_curriculum_term(env) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/mdp/commands/typing_commands.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/mdp/commands/typing_commands.py index 9c057496106b..061b0f773db4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/mdp/commands/typing_commands.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/mdp/commands/typing_commands.py @@ -952,7 +952,7 @@ def _approach_target_quat(self, ee_quat_w: torch.Tensor) -> torch.Tensor: position, so it need not be exactly reachable on the 5-DoF arm. Args: - ee_quat_w: Current moving-jaw link orientation (w, x, y, z), shape ``(num_envs, 4)``. + ee_quat_w: Current moving-jaw link orientation (x, y, z, w), shape ``(num_envs, 4)``. """ finger = quat_apply(ee_quat_w, self._ik_finger_axis) # current finger axis in world heading = finger[:, :2] / torch.linalg.norm(finger[:, :2], dim=-1, keepdim=True).clamp_min(1.0e-6) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/mdp/actions.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/mdp/actions.py index 87996ca9b066..178eadc15659 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/mdp/actions.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/mdp/actions.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Action terms for the loco-manipulation pick-and-place environment.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/mdp/observations.py index 42fe9d9249e2..92654b636da6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/locomanip_pick_place/mdp/observations.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Observation terms for the loco-manipulation pick-and-place environment.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/navigation/mdp/pre_trained_policy_action.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/navigation/mdp/pre_trained_policy_action.py index 4857d63711e1..50f34b1faba1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/navigation/mdp/pre_trained_policy_action.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/navigation/mdp/pre_trained_policy_action.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Action term wrapping a pre-trained low-level locomotion policy.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/navigation/mdp/pre_trained_policy_action_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/navigation/mdp/pre_trained_policy_action_cfg.py index 0c7b54152dfb..6fa5bd84fd78 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/navigation/mdp/pre_trained_policy_action_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/navigation/mdp/pre_trained_policy_action_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the pre-trained low-level policy action.""" + from dataclasses import MISSING from isaaclab.managers import ActionTermCfg, ObservationGroupCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/navigation/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/navigation/mdp/rewards.py index 8490bbb527bb..f69016490287 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/navigation/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/navigation/mdp/rewards.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Reward terms for the navigation environment.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/nist/mdp/curriculums.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/nist/mdp/curriculums.py index 26c74492c18f..b8ed5dd59d87 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/nist/mdp/curriculums.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/nist/mdp/curriculums.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Curriculum terms for the NIST assembly environments.""" + from __future__ import annotations from collections.abc import Sequence diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/nist/mdp/events.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/nist/mdp/events.py index ee6679c73b6c..c9c1173d5f54 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/nist/mdp/events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/nist/mdp/events.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Event terms for the NIST assembly environments.""" + from __future__ import annotations from collections.abc import Generator diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/mdp/observations.py index cedc6507d78c..2e0cba333278 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/mdp/observations.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Observation terms for the pick-and-place environments.""" + from __future__ import annotations import warnings diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/mdp/pick_place_events.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/mdp/pick_place_events.py index f1b4bd15de97..1eff3e4a5e8a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/mdp/pick_place_events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/mdp/pick_place_events.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Event terms for the pick-and-place environments.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/place/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/place/mdp/observations.py index 75a6fd950b5b..ec974d29fa4b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/place/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/place/mdp/observations.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Observation terms for the place environments.""" + from __future__ import annotations from typing import TYPE_CHECKING, Literal diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/franka_stack_events.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/franka_stack_events.py index 48b0c86128b7..b8dfbf291ef4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/franka_stack_events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/franka_stack_events.py @@ -3,12 +3,9 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Backward-compatible re-exports of the (now robot-neutral) stack event functions. +"""Backward-compatible re-exports of the robot-neutral stack event terms in :mod:`.stack_events`. -These functions used to live here but are robot-agnostic and shared by every stack robot config, -so they were moved to :mod:`.stack_events`. This module re-exports them so existing imports -(``from isaaclab_tasks.contrib.stack.mdp import franka_stack_events``) keep working. New configs -should import from :mod:`.stack_events` directly. +New configurations should import from :mod:`.stack_events` directly. """ from .stack_events import ( diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/observations.py index db480ebfbc04..cdeb9172e801 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/observations.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Observation terms for the cube-stacking environments.""" + from __future__ import annotations from typing import TYPE_CHECKING, Literal diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/stack_events.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/stack_events.py index 6f8f87abe459..8bad32c07bd7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/stack_events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/stack_events.py @@ -3,12 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Robot-neutral event functions shared by the cube-stacking tasks. - -These were previously defined in ``franka_stack_events.py``; they are robot-agnostic and are used -by every stack robot config, so they live here and ``franka_stack_events`` re-exports them for -backward compatibility. -""" +"""Robot-neutral event terms shared by the cube-stacking tasks.""" from __future__ import annotations @@ -317,14 +312,13 @@ def randomize_visual_texture_material( not env.cfg.eval_mode or env.cfg.eval_type not in [f"{asset_cfg.name}_texture", "all"] ): return - # textures = [default_texture] # enable replicator extension if not already enabled - from isaaclab.sim.utils import enable_extension + from isaaclab.sim.utils import enable_extension # noqa: PLC0415 enable_extension("omni.replicator.core") # we import the module here since we may not always need the replicator - import omni.replicator.core as rep + import omni.replicator.core as rep # noqa: PLC0415 # check to make sure replicate_physics is set to False, else raise error # note: We add an explicit check here since texture randomization can happen outside of 'prestartup' mode diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env.py index d8f8b288e27c..e91913ed58b4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Direct-workflow cabinet-opening environment.""" + from __future__ import annotations from collections.abc import Sequence @@ -51,13 +53,11 @@ def __init__(self, cfg: CabinetDirectEnvCfg, render_mode: str | None = None, **k self.arm_joint_targets = torch.zeros((self.num_envs, len(self.arm_joint_ids)), device=self.device) self.finger_joint_targets = torch.zeros((self.num_envs, len(self.finger_joint_ids)), device=self.device) - def _repeat(value: tuple[float, ...]) -> torch.Tensor: - return torch.tensor(value, device=self.device, dtype=torch.float32).repeat((self.num_envs, 1)) - - self.ee_pos_offset = _repeat(self.cfg.ee_pos_offset) - self.finger_pos_offset = _repeat(self.cfg.finger_pos_offset) - self.drawer_handle_pos_offset = _repeat(self.cfg.drawer_handle_pos_offset) - self.drawer_handle_rot_offset = _repeat(self.cfg.drawer_handle_rot_offset) + # frame offsets, broadcast to all environments + self.ee_pos_offset = self._repeat_per_env(self.cfg.ee_pos_offset) + self.finger_pos_offset = self._repeat_per_env(self.cfg.finger_pos_offset) + self.drawer_handle_pos_offset = self._repeat_per_env(self.cfg.drawer_handle_pos_offset) + self.drawer_handle_rot_offset = self._repeat_per_env(self.cfg.drawer_handle_rot_offset) self.ee_pos_w = torch.zeros((self.num_envs, 3), device=self.device) self.ee_quat_w = torch.zeros((self.num_envs, 4), device=self.device) @@ -107,10 +107,31 @@ def _apply_action(self) -> None: joint_ids=self.finger_joint_ids, ) - def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]: - terminated = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) - time_out = self.episode_length_buf >= self.max_episode_length - return terminated, time_out + def _get_observations(self) -> dict[str, torch.Tensor]: + robot_joint_pos = self._robot.data.joint_pos.torch - self._robot.data.default_joint_pos.torch + robot_joint_vel = self._robot.data.joint_vel.torch - self._robot.data.default_joint_vel.torch + drawer_joint_pos = ( + self._cabinet.data.joint_pos.torch[:, self.drawer_joint_idx] + - self._cabinet.data.default_joint_pos.torch[:, self.drawer_joint_idx] + ).unsqueeze(-1) + drawer_joint_vel = ( + self._cabinet.data.joint_vel.torch[:, self.drawer_joint_idx] + - self._cabinet.data.default_joint_vel.torch[:, self.drawer_joint_idx] + ).unsqueeze(-1) + relative_ee_drawer_distance = self.drawer_handle_pos_w - self.ee_pos_w + + observation = torch.cat( + ( + robot_joint_pos, + robot_joint_vel, + drawer_joint_pos, + drawer_joint_vel, + relative_ee_drawer_distance, + torch.clamp(self.actions, -5.0, 5.0), + ), + dim=-1, + ) + return {"policy": observation} def _get_rewards(self) -> torch.Tensor: self._compute_intermediate_values() @@ -179,6 +200,11 @@ def _get_rewards(self) -> torch.Tensor: self._episode_reward_sums[name] += value return reward + def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]: + terminated = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + time_out = self.episode_length_buf >= self.max_episode_length + return terminated, time_out + def _reset_idx(self, env_ids: Sequence[int] | None) -> None: if env_ids is None: env_ids = torch.arange(self.num_envs, device=self.device) @@ -198,32 +224,6 @@ def _reset_idx(self, env_ids: Sequence[int] | None) -> None: self._best_drawer_pos[env_ids] = 0.0 self._compute_intermediate_values(env_ids) - def _get_observations(self) -> dict[str, torch.Tensor]: - robot_joint_pos = self._robot.data.joint_pos.torch - self._robot.data.default_joint_pos.torch - robot_joint_vel = self._robot.data.joint_vel.torch - self._robot.data.default_joint_vel.torch - drawer_joint_pos = ( - self._cabinet.data.joint_pos.torch[:, self.drawer_joint_idx] - - self._cabinet.data.default_joint_pos.torch[:, self.drawer_joint_idx] - ).unsqueeze(-1) - drawer_joint_vel = ( - self._cabinet.data.joint_vel.torch[:, self.drawer_joint_idx] - - self._cabinet.data.default_joint_vel.torch[:, self.drawer_joint_idx] - ).unsqueeze(-1) - relative_ee_drawer_distance = self.drawer_handle_pos_w - self.ee_pos_w - - observation = torch.cat( - ( - robot_joint_pos, - robot_joint_vel, - drawer_joint_pos, - drawer_joint_vel, - relative_ee_drawer_distance, - torch.clamp(self.actions, -5.0, 5.0), - ), - dim=-1, - ) - return {"policy": observation} - def _compute_intermediate_values(self, env_ids: Sequence[int] | None = None) -> None: if env_ids is None: env_ids = slice(None) @@ -260,3 +260,7 @@ def _compute_intermediate_values(self, env_ids: Sequence[int] | None = None) -> self.drawer_handle_pos_offset[env_ids], self.drawer_handle_rot_offset[env_ids], ) + + def _repeat_per_env(self, value: tuple[float, ...]) -> torch.Tensor: + """Return ``value`` as a float tensor repeated for every environment, shape ``(num_envs, len(value))``.""" + return torch.tensor(value, device=self.device, dtype=torch.float32).repeat((self.num_envs, 1)) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env_cfg.py index 784a7beb0aab..ef8e836cfccf 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Base configuration for the direct-workflow cabinet-opening environment.""" + from __future__ import annotations from dataclasses import MISSING diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_env_cfg.py index 9977401cf507..3488fc8e3d08 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_env_cfg.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Base configuration for the manager-based cabinet-opening environment.""" from dataclasses import MISSING @@ -33,8 +34,13 @@ import isaaclab_tasks.core.cabinet.mdp as mdp from isaaclab_tasks.utils import PresetCfg +## +# Scene assets +## + FRAME_MARKER_SMALL_CFG = FRAME_MARKER_CFG.copy() FRAME_MARKER_SMALL_CFG.markers["frame"].scale = (0.10, 0.10, 0.10) +"""Frame marker for the end-effector and drawer-handle frame transformers.""" CABINET_CFG = ArticulationCfg( prim_path="{ENV_REGEX_NS}/Cabinet", @@ -84,6 +90,11 @@ """Shared dome-light configuration.""" +## +# Simulation presets +## + + @configclass class CabinetSimCfg(PresetCfg): """Simulation configuration presets for the cabinet environment. @@ -145,8 +156,8 @@ class CabinetDecimationCfg(PresetCfg): class CabinetSceneCfg(InteractiveSceneCfg): """Configuration for the cabinet scene with a robot and a cabinet. - This is the abstract base implementation, the exact scene is defined in the derived classes - which need to set the robot and end-effector frames + This is the abstract base implementation. The exact scene is defined in the derived classes, + which need to set the robot and end-effector frames. """ # robot and end-effector frames -- set by a robot-specific subclass @@ -268,11 +279,11 @@ class EventCfg: class RewardsCfg: """Reward terms for the MDP.""" - # 1. Approach the handle + # (1) approach the handle approach_ee_handle = RewTerm(func=mdp.approach_ee_handle, weight=2.0, params={"threshold": 0.2}) align_ee_handle = RewTerm(func=mdp.align_ee_handle, weight=0.5) - # 2. Grasp the handle + # (2) grasp the handle approach_gripper_handle = RewTerm(func=mdp.approach_gripper_handle, weight=5.0, params={"offset": MISSING}) align_grasp_around_handle = RewTerm(func=mdp.align_grasp_around_handle, weight=0.125) grasp_handle = RewTerm( @@ -285,7 +296,7 @@ class RewardsCfg: }, ) - # 3. Open the drawer + # (3) open the drawer open_drawer_bonus = RewTerm( func=mdp.open_drawer_bonus, weight=7.5, @@ -300,7 +311,7 @@ class RewardsCfg: params={"asset_cfg": SceneEntityCfg("cabinet", joint_names=["drawer_top_joint"])}, ) - # 4. Penalize actions for cosmetic reasons + # (4) penalize actions for cosmetic reasons action_rate_l2 = RewTerm(func=mdp.action_rate_l2, weight=-1e-2) joint_vel = RewTerm(func=mdp.joint_vel_l2, weight=-0.0001) @@ -321,7 +332,9 @@ class TerminationsCfg: class CabinetEnvCfg(ManagerBasedRLEnvCfg): """Configuration for the cabinet environment.""" + # Simulation settings: the time step and physics vary per backend sim: CabinetSimCfg = CabinetSimCfg() + decimation: int = CabinetDecimationCfg() # Scene settings scene: CabinetSceneCfg = CabinetSceneCfg(num_envs=4096, env_spacing=2.0) # Basic settings @@ -332,10 +345,7 @@ class CabinetEnvCfg(ManagerBasedRLEnvCfg): terminations: TerminationsCfg = TerminationsCfg() events: EventCfg = EventCfg() - decimation: int = CabinetDecimationCfg() - def __post_init__(self): """Post initialization.""" # general settings self.episode_length_s = 8.0 - # simulation settings are defined in CabinetSimCfg (dt/physics vary per backend) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/__init__.py index 860db87e940d..19673aa3899c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/__init__.py @@ -3,40 +3,42 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Franka cabinet-opening environments (direct and manager-based workflows).""" + import gymnasium as gym -from isaaclab_tasks.core.cabinet.config.franka import agents +from . import agents ## -# Register Gym environments -- manager-based workflow. +# Register Gym environments -- direct workflow. ## gym.register( - id="Isaac-Open-Drawer-Franka", - entry_point="isaaclab.envs:ManagerBasedRLEnv", + id="Isaac-Open-Drawer-Franka-Direct", + entry_point="isaaclab_tasks.core.cabinet.cabinet_direct_env:CabinetDirectEnv", + disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:FrankaCabinetEnvCfg", - "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CabinetPPORunnerCfg", + "env_cfg_entry_point": f"{__name__}.cabinet_direct_env_cfg:FrankaCabinetDirectEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_direct_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:FrankaCabinetPPORunnerCfg", "default_agent": "rsl_rl", - "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_manager_ppo_cfg.yaml", - "skrl_cfg_entry_point": f"{agents.__name__}:skrl_manager_ppo_cfg.yaml", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_direct_ppo_cfg.yaml", }, - disable_env_checker=True, ) ## -# Register Gym environments -- direct workflow. +# Register Gym environments -- manager-based workflow. ## gym.register( - id="Isaac-Open-Drawer-Franka-Direct", - entry_point="isaaclab_tasks.core.cabinet.cabinet_direct_env:CabinetDirectEnv", + id="Isaac-Open-Drawer-Franka", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.cabinet_direct_env_cfg:FrankaCabinetDirectEnvCfg", - "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_direct_ppo_cfg.yaml", - "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:FrankaCabinetPPORunnerCfg", + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:FrankaCabinetEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_manager_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CabinetPPORunnerCfg", "default_agent": "rsl_rl", - "skrl_cfg_entry_point": f"{agents.__name__}:skrl_direct_ppo_cfg.yaml", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_manager_ppo_cfg.yaml", }, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/cabinet_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/cabinet_direct_env_cfg.py index 6298bb6bf218..1e3516d16724 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/cabinet_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/cabinet_direct_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the direct-workflow Franka cabinet-opening environment.""" + from __future__ import annotations from isaaclab.utils import configclass diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/joint_pos_env_cfg.py index a9331cefede8..5b2930b9a7b7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/joint_pos_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the manager-based Franka cabinet-opening environment.""" + from isaaclab.sensors import FrameTransformerCfg from isaaclab.sensors.frame_transformer import OffsetCfg from isaaclab.utils import configclass @@ -50,6 +52,8 @@ class FrankaCabinetSceneCfg(CabinetSceneCfg): @configclass class FrankaCabinetEnvCfg(CabinetEnvCfg): + """Cabinet-opening environment with a Franka Panda arm driven by joint position targets.""" + scene: FrankaCabinetSceneCfg = FrankaCabinetSceneCfg(num_envs=4096, env_spacing=2.0) def __post_init__(self): @@ -76,6 +80,5 @@ def __post_init__(self): def play_mode(self): super().play_mode() - # make a smaller scene for play self.scene.env_spacing = 2.5 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/__init__.py index 3feb2d711bc5..17a0804d9f16 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/__init__.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module contains the functions that are specific to the cabinet environments.""" +"""MDP terms specific to the cabinet environments.""" from isaaclab.utils.module import lazy_export diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/__init__.pyi index 6ac9aca46945..68fe694bda48 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/__init__.pyi @@ -17,8 +17,8 @@ __all__ = [ "rel_ee_drawer_distance", ] -from isaaclab_tasks.core.cabinet.mdp.observations import ee_pos, ee_quat, fingertips_pos, rel_ee_drawer_distance -from isaaclab_tasks.core.cabinet.mdp.rewards import ( +from .observations import ee_pos, ee_quat, fingertips_pos, rel_ee_drawer_distance +from .rewards import ( align_ee_handle, align_grasp_around_handle, approach_ee_handle, @@ -27,4 +27,5 @@ from isaaclab_tasks.core.cabinet.mdp.rewards import ( multi_stage_open_drawer, open_drawer_bonus, ) + from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/observations.py index 219ce40d991f..822c88e19473 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/observations.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Observation terms for the cabinet environments.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/rewards.py index 93a9eef8dca5..8d47a3dabb64 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/mdp/rewards.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Reward terms for the cabinet environments.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/__init__.py index b36e7ed3de52..99b7549a6463 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/__init__.py @@ -3,12 +3,11 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Cartpole balancing environment. +"""Cartpole balancing environments. -This package consolidates the direct-workflow and manager-based-workflow -cartpole tasks. Module files carry a ``_direct_`` or ``_manager_`` infix to -disambiguate the two workflows within the flat package layout. +This package consolidates the direct-workflow and manager-based-workflow cartpole tasks. Module files +carry a ``_direct_`` or ``_manager_`` infix to disambiguate the two workflows within the flat package +layout. """ import gymnasium as gym diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_common.py new file mode 100644 index 000000000000..6c02e97ba33a --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_common.py @@ -0,0 +1,88 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configurations shared by the direct and manager-based cartpole environments.""" + +from isaaclab_newton.physics import KaminoPADMMSolverCfg, MJWarpSolverCfg, NewtonCfg +from isaaclab_ov.physics import OvPhysxCfg +from isaaclab_physx.physics import PhysxCfg + +import isaaclab.sim as sim_utils +from isaaclab.physics import PhysxAutoCfg +from isaaclab.sensors import CameraCfg +from isaaclab.utils import configclass + +from isaaclab_tasks.utils import PresetCfg +from isaaclab_tasks.utils.presets import MultiBackendRendererCfg + +LIGHT_ORIENTATION: tuple[float, float, float, float] = ( + -0.14644663035869598, + -0.3535534143447876, + -0.3535534143447876, + 0.8535533547401428, +) +"""Distant light orientation as an ``(x, y, z, w)`` quaternion for euler angles (0, -45, -45) degrees.""" + + +@configclass +class CartpolePhysicsCfg(PresetCfg): + """Physics backend presets for the cartpole environments.""" + + isaacsim_physx: PhysxCfg = PhysxCfg() + ovphysx: OvPhysxCfg = OvPhysxCfg() + physx: PhysxAutoCfg = PhysxAutoCfg(isaacsim_physx=isaacsim_physx, ovphysx=ovphysx) + newton_mjwarp: NewtonCfg = NewtonCfg( + solver_cfg=MJWarpSolverCfg( + njmax=5, + nconmax=3, + cone="pyramidal", + impratio=1, + integrator="implicitfast", + ), + num_substeps=1, + debug_mode=False, + use_cuda_graph=True, + ) + newton_kamino: NewtonCfg = NewtonCfg( + solver_cfg=KaminoPADMMSolverCfg(sparse_jacobian=True), + debug_mode=False, + use_cuda_graph=True, + ) + default: NewtonCfg = newton_mjwarp + + +@configclass +class CartpoleTiledCameraCfg(PresetCfg): + """Tiled-camera presets, one per rendered data type. + + Each variant selects its rendering backend (RTX, OmniverseRTX, Newton + Warp) through the + nested :attr:`~BaseCartpoleTiledCameraCfg.renderer_cfg` preset, so a single ``presets=`` selector + can pick both the data type and the backend. + """ + + @configclass + class BaseCartpoleTiledCameraCfg(CameraCfg): + """Camera looking at the cartpole from the side.""" + + prim_path: str = "{ENV_REGEX_NS}/Camera" + offset: CameraCfg.OffsetCfg = CameraCfg.OffsetCfg( + pos=(-5.0, 0.0, 2.0), rot=(0.0, 0.0, 0.0, 1.0), convention="world" + ) + data_types: list[str] = [] + spawn: sim_utils.PinholeCameraCfg = sim_utils.PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) + ) + width: int = 96 + height: int = 96 + renderer_cfg: MultiBackendRendererCfg = MultiBackendRendererCfg() + + default = BaseCartpoleTiledCameraCfg(data_types=["rgb"]) + depth = BaseCartpoleTiledCameraCfg(data_types=["depth"]) + albedo = BaseCartpoleTiledCameraCfg(data_types=["albedo"]) + semantic_segmentation = BaseCartpoleTiledCameraCfg(data_types=["semantic_segmentation"]) + simple_shading_constant_diffuse = BaseCartpoleTiledCameraCfg(data_types=["simple_shading_constant_diffuse"]) + simple_shading_diffuse_mdl = BaseCartpoleTiledCameraCfg(data_types=["simple_shading_diffuse_mdl"]) + simple_shading_full_mdl = BaseCartpoleTiledCameraCfg(data_types=["simple_shading_full_mdl"]) + rgb = default diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env.py index e5420c63e296..06a3bbfe7b71 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Direct-workflow cartpole environment driven by camera observations.""" + from __future__ import annotations from collections.abc import Sequence @@ -45,8 +47,8 @@ def __init__(self, cfg: CartpoleCameraEnvCfg, render_mode: str | None = None, ** self._stack: CircularBuffer | None = None if self.cfg.frame_stack > 1: - # Channel-stack mode: buffer storage is laid out so that .stacked is a free - # contiguous reshape into (B, K*C, H, W) -- no per-step permute/reshape alloc. + # channel-stack mode: the buffer storage is laid out so that ``stacked`` is a free + # contiguous reshape into (B, K*C, H, W) without a per-step permute or reshape self._stack = CircularBuffer( max_len=self.cfg.frame_stack, batch_size=self.num_envs, device=self.device, stack_dim=1 ) @@ -57,10 +59,10 @@ def _get_observations(self) -> dict: rgb_like = is_rgb_like(data_type) segmentation = data_type == "semantic_segmentation" - # Defer normalize past the ring buffer when stacking RGB-like data so the ring holds - # uint8 (4x cheaper per-step copies). Math is identical -- K frames live in disjoint - # channel slices of (B, K*C, H, W). Colorized segmentation is uint8 RGBA and qualifies; - # non-colorized segmentation is an int32 label map and does not. + # defer normalization past the ring buffer when stacking RGB-like data so the ring holds + # uint8 (4x cheaper per-step copies); the math is identical since the K frames live in + # disjoint channel slices of (B, K*C, H, W). Colorized segmentation is uint8 RGBA and + # qualifies; non-colorized segmentation is an int32 label map and does not. defer_normalize = self._stack is not None and (rgb_like or (segmentation and camera_data.dtype == torch.uint8)) if data_type == "albedo": @@ -79,12 +81,10 @@ def _get_observations(self) -> dict: obs = self._stack.stacked if defer_normalize: - # No ``out=`` -- a fresh float32 tensor is allocated per call. The caching - # allocator returns a different block than the previous step's (still - # referenced by the trainer), so the previous-iteration ``observations`` - # is not overwritten before ``record_transition`` reads it. See - # :func:`isaaclab.utils.warp.ops.normalize_image_uint8` for the aliasing - # hazard documentation. + # no ``out=``: a fresh float32 tensor is allocated per call, so the previous step's + # observations (still referenced by the trainer) are not overwritten before + # ``record_transition`` reads them. See :func:`isaaclab.utils.warp.ops.normalize_image_uint8` + # for the aliasing hazard. obs = normalize_camera_image(obs, data_type, channel_dim=1) elif self._stack is not None: # ``stacked`` is a view of the ring buffer storage which is overwritten on @@ -94,8 +94,7 @@ def _get_observations(self) -> dict: if self.cfg.write_image_to_file: save_images_to_file(self._tiled_camera.data.output[data_type] / 255.0, f"cartpole_{data_type}.png") - critic_obs = super()._get_observations()["policy"] - return {"policy": obs, "critic": critic_obs} + return {"policy": obs, "critic": super()._get_observations()["policy"]} def _reset_idx(self, env_ids: Sequence[int] | None): super()._reset_idx(env_ids) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py index 9473cc3df45c..5b406b396a7b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py @@ -3,44 +3,18 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the direct-workflow cartpole camera environment.""" + from __future__ import annotations import math -import isaaclab.sim as sim_utils -from isaaclab.sensors import CameraCfg from isaaclab.utils import configclass from isaaclab.visualizers import VisualizerCfg +from isaaclab_tasks.core.cartpole.cartpole_common import CartpoleTiledCameraCfg from isaaclab_tasks.core.cartpole.cartpole_direct_env_cfg import CartpoleEnvCfg, CartpoleSceneCfg from isaaclab_tasks.utils import PresetCfg -from isaaclab_tasks.utils.presets import MultiBackendRendererCfg - - -@configclass -class CartpoleTiledCameraCfg(PresetCfg): - @configclass - class BaseCartpoleTiledCameraCfg(CameraCfg): - prim_path: str = "{ENV_REGEX_NS}/Camera" - offset: CameraCfg.OffsetCfg = CameraCfg.OffsetCfg( - pos=(-5.0, 0.0, 2.0), rot=(0.0, 0.0, 0.0, 1.0), convention="world" - ) - data_types: list[str] = [] - spawn: sim_utils.PinholeCameraCfg = sim_utils.PinholeCameraCfg( - focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) - ) - width: int = 96 - height: int = 96 - renderer_cfg: MultiBackendRendererCfg = MultiBackendRendererCfg() - - default = BaseCartpoleTiledCameraCfg(data_types=["rgb"]) - depth = BaseCartpoleTiledCameraCfg(data_types=["depth"]) - albedo = BaseCartpoleTiledCameraCfg(data_types=["albedo"]) - semantic_segmentation = BaseCartpoleTiledCameraCfg(data_types=["semantic_segmentation"]) - simple_shading_constant_diffuse = BaseCartpoleTiledCameraCfg(data_types=["simple_shading_constant_diffuse"]) - simple_shading_diffuse_mdl = BaseCartpoleTiledCameraCfg(data_types=["simple_shading_diffuse_mdl"]) - simple_shading_full_mdl = BaseCartpoleTiledCameraCfg(data_types=["simple_shading_full_mdl"]) - rgb = default @configclass @@ -53,9 +27,15 @@ class CartpoleCameraSceneCfg(CartpoleSceneCfg): @configclass class CartpoleCameraEnvCfg(PresetCfg): + """Cartpole camera environment with a selectable camera data type. + + The selector also picks the matching tiled camera preset and its rendering backend through + :attr:`CartpoleCameraSceneCfg.tiled_camera`. + """ + @configclass class BaseCartpoleCameraEnvCfg(CartpoleEnvCfg): - """Camera variant of :class:`CartpoleEnvCfg` — only the fields that differ are overridden.""" + """Camera variant of :class:`CartpoleEnvCfg`; only the fields that differ are overridden.""" write_image_to_file = False @@ -78,6 +58,7 @@ class BaseCartpoleCameraEnvCfg(CartpoleEnvCfg): initial_pole_angle_range = (-0.125 * math.pi, 0.125 * math.pi) # [rad] def __post_init__(self): + super().__post_init__() self.sim.default_visualizer_cfg = VisualizerCfg(eye=(20.0, 20.0, 20.0)) default = BaseCartpoleCameraEnvCfg() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env.py index 5d4ac92bd037..085ad2c13033 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Direct-workflow cartpole balancing environment.""" + from __future__ import annotations from collections.abc import Sequence @@ -51,11 +53,10 @@ def _get_observations(self) -> dict: ), dim=-1, ) - observations = {"policy": obs} - return observations + return {"policy": obs} def _get_rewards(self) -> torch.Tensor: - total_reward = compute_rewards( + return compute_rewards( self.cfg.rew_scale_alive, self.cfg.rew_scale_terminated, self.cfg.rew_scale_pole_pos, @@ -67,7 +68,6 @@ def _get_rewards(self) -> torch.Tensor: self.reset_terminated, self.step_dt, ) - return total_reward def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]: self.joint_pos = self.cartpole.data.joint_pos.torch @@ -81,7 +81,7 @@ def _reset_idx(self, env_ids: Sequence[int] | None): if env_ids is None: env_ids = self.cartpole._ALL_INDICES - # Log survival success rate before resetting + # log the survival success rate before resetting (survived = timed out without terminating early) survived = self.reset_time_outs[env_ids].float() self.extras.setdefault("log", {})["Metrics/success_rate"] = survived.mean().item() @@ -145,7 +145,7 @@ def compute_rewards( cart_vel: torch.Tensor, reset_terminated: torch.Tensor, step_dt: float, -): +) -> torch.Tensor: pole_pos = wrap_to_pi(pole_pos) rew_alive = rew_scale_alive * (1.0 - reset_terminated.float()) rew_termination = rew_scale_terminated * reset_terminated.float() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env_cfg.py index 87312ea8a784..10b447f84d9f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env_cfg.py @@ -3,57 +3,25 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the direct-workflow cartpole environment.""" + from __future__ import annotations import math -from isaaclab_newton.physics import ( - KaminoPADMMSolverCfg, - MJWarpSolverCfg, - NewtonCfg, -) -from isaaclab_ov.physics import OvPhysxCfg -from isaaclab_physx.physics import PhysxCfg - import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, AssetBaseCfg from isaaclab.envs import DirectRLEnvCfg -from isaaclab.physics import PhysxAutoCfg from isaaclab.scene import InteractiveSceneCfg from isaaclab.sim import SimulationCfg from isaaclab.utils import configclass from isaaclab.visualizers import VisualizerCfg -from isaaclab_tasks.utils import PresetCfg +from isaaclab_tasks.core.cartpole.cartpole_common import LIGHT_ORIENTATION, CartpolePhysicsCfg from isaaclab_assets.robots.cartpole import CARTPOLE_CFG -@configclass -class CartpolePhysicsCfg(PresetCfg): - isaacsim_physx: PhysxCfg = PhysxCfg() - ovphysx: OvPhysxCfg = OvPhysxCfg() - physx: PhysxAutoCfg = PhysxAutoCfg(isaacsim_physx=isaacsim_physx, ovphysx=ovphysx) - newton_mjwarp: NewtonCfg = NewtonCfg( - solver_cfg=MJWarpSolverCfg( - njmax=5, - nconmax=3, - cone="pyramidal", - impratio=1, - integrator="implicitfast", - ), - num_substeps=1, - debug_mode=False, - use_cuda_graph=True, - ) - newton_kamino: NewtonCfg = NewtonCfg( - solver_cfg=KaminoPADMMSolverCfg(sparse_jacobian=True), - debug_mode=False, - use_cuda_graph=True, - ) - default = newton_mjwarp - - @configclass class CartpoleSceneCfg(InteractiveSceneCfg): """Cartpole assets constructed and cloned as one scene.""" @@ -63,14 +31,14 @@ class CartpoleSceneCfg(InteractiveSceneCfg): light = AssetBaseCfg( prim_path="/World/Light", spawn=sim_utils.DistantLightCfg(intensity=2000.0), - init_state=AssetBaseCfg.InitialStateCfg( - rot=(-0.14644663035869598, -0.3535534143447876, -0.3535534143447876, 0.8535533547401428) - ), + init_state=AssetBaseCfg.InitialStateCfg(rot=LIGHT_ORIENTATION), ) @configclass class CartpoleEnvCfg(DirectRLEnvCfg): + """Configuration for the direct-workflow cartpole balancing environment.""" + # env decimation = 2 episode_length_s = 5.0 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_camera_env_cfg.py index bcb211aa9751..cfe416e10c8a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_camera_env_cfg.py @@ -3,58 +3,20 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the manager-based cartpole camera environment.""" + import math -import isaaclab.sim as sim_utils from isaaclab.managers import ObservationGroupCfg as ObsGroup from isaaclab.managers import ObservationTermCfg as ObsTerm from isaaclab.managers import SceneEntityCfg -from isaaclab.sensors import CameraCfg from isaaclab.utils import configclass from isaaclab.visualizers import VisualizerCfg import isaaclab_tasks.core.cartpole.mdp as mdp +from isaaclab_tasks.core.cartpole.cartpole_common import CartpoleTiledCameraCfg from isaaclab_tasks.core.cartpole.cartpole_manager_env_cfg import CartpoleEnvCfg, CartpoleSceneCfg, ObservationsCfg from isaaclab_tasks.utils import PresetCfg -from isaaclab_tasks.utils.presets import MultiBackendRendererCfg - -## -# Camera presets -## - - -@configclass -class CartpoleTiledCameraCfg(PresetCfg): - """Tiled-camera presets, one per rendered data type. - - Each variant selects its rendering backend (RTX, OmniverseRTX, Newton + Warp) through the - nested :attr:`~BaseCartpoleTiledCameraCfg.renderer_cfg` preset, so a single ``presets=`` selector - can pick both the data type and the backend. - """ - - @configclass - class BaseCartpoleTiledCameraCfg(CameraCfg): - prim_path: str = "{ENV_REGEX_NS}/Camera" - offset: CameraCfg.OffsetCfg = CameraCfg.OffsetCfg( - pos=(-5.0, 0.0, 2.0), rot=(0.0, 0.0, 0.0, 1.0), convention="world" - ) - data_types: list[str] = [] - spawn: sim_utils.PinholeCameraCfg = sim_utils.PinholeCameraCfg( - focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) - ) - width: int = 96 - height: int = 96 - renderer_cfg: MultiBackendRendererCfg = MultiBackendRendererCfg() - - default = BaseCartpoleTiledCameraCfg(data_types=["rgb"]) - depth = BaseCartpoleTiledCameraCfg(data_types=["depth"]) - albedo = BaseCartpoleTiledCameraCfg(data_types=["albedo"]) - semantic_segmentation = BaseCartpoleTiledCameraCfg(data_types=["semantic_segmentation"]) - simple_shading_constant_diffuse = BaseCartpoleTiledCameraCfg(data_types=["simple_shading_constant_diffuse"]) - simple_shading_diffuse_mdl = BaseCartpoleTiledCameraCfg(data_types=["simple_shading_diffuse_mdl"]) - simple_shading_full_mdl = BaseCartpoleTiledCameraCfg(data_types=["simple_shading_full_mdl"]) - rgb = default - ## # Scene definition @@ -124,7 +86,7 @@ class TheiaTinyObservationCfg: @configclass class TheiaTinyFeaturesCameraPolicyCfg(ObsGroup): - """Observations for policy group with features extracted from RGB images with a frozen Theia-Tiny Transformer""" + """Observations for policy group with features extracted from RGB images with a frozen Theia-Tiny model.""" image = ObsTerm( func=mdp.image_features, @@ -156,7 +118,7 @@ class CartpoleCameraEnvCfg(PresetCfg): @configclass class BaseCartpoleCameraEnvCfg(CartpoleEnvCfg): - """Camera variant of :class:`CartpoleEnvCfg` -- only the fields that differ are overridden.""" + """Camera variant of :class:`CartpoleEnvCfg`; only the fields that differ are overridden.""" frame_stack: int = 2 """Number of frames to stack along the channel dimension. @@ -169,10 +131,11 @@ class BaseCartpoleCameraEnvCfg(CartpoleEnvCfg): def __post_init__(self): super().__post_init__() - # remove ground as it obstructs the camera + # remove the ground as it obstructs the camera self.scene.ground = None + # reset: smaller initial pole angle than the proprioceptive task self.events.reset_pole_position.params["position_range"] = (-0.125 * math.pi, 0.125 * math.pi) - # visualizer camera settings + # visualizer settings self.sim.default_visualizer_cfg = VisualizerCfg(eye=(20.0, 20.0, 20.0), lookat=(0.0, 0.0, 0.0)) rgb = BaseCartpoleCameraEnvCfg(observations=image_observations_cfg("rgb")) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_env_cfg.py index e8432ae1f673..8b096733ed8d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_env_cfg.py @@ -3,15 +3,9 @@ # # SPDX-License-Identifier: BSD-3-Clause -import math +"""Configuration for the manager-based cartpole environment.""" -from isaaclab_newton.physics import ( - KaminoPADMMSolverCfg, - MJWarpSolverCfg, - NewtonCfg, -) -from isaaclab_ov.physics import OvPhysxCfg -from isaaclab_physx.physics import PhysxCfg +import math import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, AssetBaseCfg @@ -22,46 +16,14 @@ from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.managers import SceneEntityCfg from isaaclab.managers import TerminationTermCfg as DoneTerm -from isaaclab.physics import PhysxAutoCfg from isaaclab.scene import InteractiveSceneCfg from isaaclab.utils import configclass from isaaclab.visualizers import VisualizerCfg import isaaclab_tasks.core.cartpole.mdp as mdp -from isaaclab_tasks.utils import PresetCfg - -from isaaclab_assets.robots.cartpole import CARTPOLE_CFG # isort:skip - - -## -# Physics backend presets -## - - -@configclass -class CartpolePhysicsCfg(PresetCfg): - isaacsim_physx: PhysxCfg = PhysxCfg() - ovphysx: OvPhysxCfg = OvPhysxCfg() - physx: PhysxAutoCfg = PhysxAutoCfg(isaacsim_physx=isaacsim_physx, ovphysx=ovphysx) - newton_mjwarp: NewtonCfg = NewtonCfg( - solver_cfg=MJWarpSolverCfg( - njmax=5, - nconmax=3, - cone="pyramidal", - impratio=1, - integrator="implicitfast", - ), - num_substeps=1, - debug_mode=False, - use_cuda_graph=True, - ) - default: NewtonCfg = newton_mjwarp - newton_kamino: NewtonCfg = NewtonCfg( - solver_cfg=KaminoPADMMSolverCfg(sparse_jacobian=True), - debug_mode=False, - use_cuda_graph=True, - ) +from isaaclab_tasks.core.cartpole.cartpole_common import LIGHT_ORIENTATION, CartpolePhysicsCfg +from isaaclab_assets.robots.cartpole import CARTPOLE_CFG ## # Scene definition @@ -82,12 +44,9 @@ class CartpoleSceneCfg(InteractiveSceneCfg): robot: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") # lights - # rot quaternion for euler angles (roll, pitch, yaw) = (0, -45, -45) degrees distant_light = AssetBaseCfg( prim_path="/World/DistantLight", - init_state=AssetBaseCfg.InitialStateCfg( - rot=(-0.14644663035869598, -0.3535534143447876, -0.3535534143447876, 0.8535533547401428) - ), + init_state=AssetBaseCfg.InitialStateCfg(rot=LIGHT_ORIENTATION), spawn=sim_utils.DistantLightCfg(color=(1.0, 1.0, 1.0), intensity=2000.0), ) @@ -116,7 +75,7 @@ class PolicyCfg(ObsGroup): joint_pos_rel = ObsTerm(func=mdp.joint_pos_rel) joint_vel_rel = ObsTerm(func=mdp.joint_vel_rel) - def __post_init__(self) -> None: + def __post_init__(self): self.enable_corruption = False self.concatenate_terms = True @@ -207,20 +166,19 @@ class CartpoleEnvCfg(ManagerBasedRLEnvCfg): # Basic settings observations: ObservationsCfg = ObservationsCfg() actions: ActionsCfg = ActionsCfg() - events: EventCfg = EventCfg() # MDP settings rewards: RewardsCfg = RewardsCfg() terminations: TerminationsCfg = TerminationsCfg() + events: EventCfg = EventCfg() - # Post initialization - def __post_init__(self) -> None: + def __post_init__(self): """Post initialization.""" # general settings self.decimation = 2 self.episode_length_s = 5 - # visualizer camera settings - self.sim.default_visualizer_cfg = VisualizerCfg(eye=(8.0, 0.0, 5.0)) # simulation settings self.sim.dt = 1 / 120 self.sim.render_interval = self.decimation self.sim.physics = CartpolePhysicsCfg() + # visualizer settings + self.sim.default_visualizer_cfg = VisualizerCfg(eye=(8.0, 0.0, 5.0)) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/__init__.py index 1ce92a1952e8..805c53477123 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/__init__.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module contains the functions that are specific to the cartpole environments.""" +"""MDP terms specific to the cartpole environments.""" from isaaclab.utils.module import lazy_export diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/__init__.pyi index 7947b2623a80..49ffda014ee7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/__init__.pyi @@ -3,12 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause -__all__ = [ - "CameraImageStack", - "joint_pos_target_l2", - "survival_success_rate", -] +__all__ = ["CameraImageStack"] + +from .observations import CameraImageStack -from isaaclab_tasks.core.cartpole.mdp.observations import CameraImageStack -from isaaclab_tasks.core.cartpole.mdp.rewards import joint_pos_target_l2, survival_success_rate from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/observations.py index 6a0c44e44d37..0168b83ed7ef 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/observations.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Observation terms for the cartpole environments.""" + from __future__ import annotations from collections.abc import Sequence diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/rewards.py deleted file mode 100644 index b2838a0cce9a..000000000000 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/rewards.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import torch - -from isaaclab.managers import ManagerTermBase, RewardTermCfg, SceneEntityCfg -from isaaclab.utils.math import wrap_to_pi - -if TYPE_CHECKING: - from isaaclab.assets import Articulation - from isaaclab.envs import ManagerBasedRLEnv - - -def joint_pos_target_l2(env: ManagerBasedRLEnv, target: float, asset_cfg: SceneEntityCfg) -> torch.Tensor: - """Penalize joint position deviation from a target value.""" - # extract the used quantities (to enable type-hinting) - asset: Articulation = env.scene[asset_cfg.name] - # wrap the joint positions to (-pi, pi) - joint_pos = wrap_to_pi(asset.data.joint_pos.torch[:, asset_cfg.joint_ids]) - # compute the reward - return torch.sum(torch.square(joint_pos - target), dim=1) - - -class survival_success_rate(ManagerTermBase): - """Tracks episode survival as the success metric. - - Returns zero reward (pure metric tracking). Flushes ``Metrics/success_rate`` - into ``extras["log"]`` on episode reset, where success = timed out without - early termination. - """ - - def __init__(self, env: ManagerBasedRLEnv, cfg: RewardTermCfg): - super().__init__(cfg, env) - - def reset(self, env_ids: torch.Tensor): - survived = self._env.termination_manager.time_outs[env_ids] - self._env.extras.setdefault("log", {})["Metrics/success_rate"] = survived.float().mean().item() - - def __call__(self, env: ManagerBasedRLEnv) -> torch.Tensor: - return torch.zeros(env.num_envs, device=env.device) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/__init__.py index 20219001d70e..0e819d02fb6d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/__init__.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module contains the functions that are specific to the fourbar-pole environments.""" +"""MDP terms specific to the fourbar-pole swing-up environment.""" from isaaclab.utils.module import lazy_export diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/__init__.pyi index eae4b097228d..4b02b3cc42a7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/__init__.pyi @@ -3,15 +3,9 @@ # # SPDX-License-Identifier: BSD-3-Clause -__all__ = [ - "joint_pos_cos", - "joint_pos_sin", - "pole_upright", -] +__all__ = ["joint_pos_cos", "joint_pos_sin", "pole_upright"] + +from .observations import joint_pos_cos, joint_pos_sin +from .rewards import pole_upright -from .rewards import ( - joint_pos_cos, - joint_pos_sin, - pole_upright, -) from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/observations.py new file mode 100644 index 000000000000..bfc001f04447 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/observations.py @@ -0,0 +1,34 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Observation terms for the fourbar-pole swing-up environment.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch + +from isaaclab.managers import SceneEntityCfg + +if TYPE_CHECKING: + from isaaclab.assets import Articulation + from isaaclab.envs import ManagerBasedRLEnv + + +def joint_pos_cos(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: + """Cosine of the selected joint positions. + + Encodes the angle without the wrap-around discontinuity at ``+-pi`` so the policy sees a smooth + signal as the pole swings through the bottom. + """ + asset: Articulation = env.scene[asset_cfg.name] + return torch.cos(asset.data.joint_pos.torch[:, asset_cfg.joint_ids]) + + +def joint_pos_sin(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: + """Sine of the selected joint positions (companion to :func:`joint_pos_cos`).""" + asset: Articulation = env.scene[asset_cfg.name] + return torch.sin(asset.data.joint_pos.torch[:, asset_cfg.joint_ids]) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/rewards.py index 98d08b022cb8..e1cc7f3edd3b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/mdp/rewards.py @@ -3,12 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Functions specific to the fourbar-pole swing-up environments. - -This module holds both observation helpers (``joint_pos_cos`` / ``joint_pos_sin``) -that encode an angle without the ``+-pi`` wrap discontinuity, and the swing-up -reward / success-metric terms. -""" +"""Reward terms for the fourbar-pole swing-up environment.""" from __future__ import annotations @@ -19,36 +14,19 @@ from isaaclab.managers import ManagerTermBase, RewardTermCfg, SceneEntityCfg if TYPE_CHECKING: - from isaaclab.assets import Articulation from isaaclab.envs import ManagerBasedRLEnv -def joint_pos_cos(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: - """Cosine of the selected joint positions. - - Encodes the angle without the wrap-around discontinuity at ``+-pi`` so the - policy sees a smooth signal as the pole swings through the bottom. - """ - asset: Articulation = env.scene[asset_cfg.name] - return torch.cos(asset.data.joint_pos.torch[:, asset_cfg.joint_ids]) - - -def joint_pos_sin(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: - """Sine of the selected joint positions (companion to :func:`joint_pos_cos`).""" - asset: Articulation = env.scene[asset_cfg.name] - return torch.sin(asset.data.joint_pos.torch[:, asset_cfg.joint_ids]) - - class pole_upright(ManagerTermBase): """Pole-uprightness reward that also logs a sustained-upright success metric. - Reward is ``sum(cos(pole_angle))`` -- ``+1`` upright, ``-1`` hanging. On reset it flushes - ``Metrics/success_rate``: the fraction of environments that held the pole within the upright - cone (``cos > success_threshold``) for at least the final ``hold_time_s`` seconds. Both params - shape only the metric, not the reward. + The reward is ``sum(cos(pole_angle))``: ``+1`` upright and ``-1`` hanging. On reset it flushes + ``Metrics/success_rate`` into ``extras["log"]``: the fraction of environments that held the pole + within the upright cone (``cos > success_threshold``) for at least the final ``hold_time_s`` + seconds. Both parameters shape only the metric, not the reward. """ - def __init__(self, env: ManagerBasedRLEnv, cfg: RewardTermCfg): + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): super().__init__(cfg, env) self._consecutive_upright = torch.zeros(env.num_envs, device=env.device) self._success = torch.zeros(env.num_envs, device=env.device) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py index 5e6e83b42a57..066b589576e7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py @@ -3,39 +3,42 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -ShadowHand Over environment. -""" +"""Shadow Hand handover environments (direct and manager-based workflows).""" import gymnasium as gym from . import agents ## -# Register Gym environments. +# Register Gym environments -- direct workflow. ## gym.register( - id="Isaac-Shadow-Handover", - entry_point="isaaclab.envs:ManagerBasedRLEnv", + id="Isaac-Shadow-Handover-Direct", + entry_point=f"{__name__}.handover_env:HandoverEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.handover_manager_env_cfg:HandoverManagerEnvCfg", + "env_cfg_entry_point": f"{__name__}.handover_env_cfg:HandoverEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:HandoverPPORunnerCfg", + "default_agent": "rsl_rl", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", + "skrl_ippo_cfg_entry_point": f"{agents.__name__}:skrl_ippo_cfg.yaml", + "skrl_mappo_cfg_entry_point": f"{agents.__name__}:skrl_mappo_cfg.yaml", }, ) +## +# Register Gym environments -- manager-based workflow. +## + gym.register( - id="Isaac-Shadow-Handover-Direct", - entry_point=f"{__name__}.handover_env:HandoverEnv", + id="Isaac-Shadow-Handover", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.handover_env_cfg:HandoverEnvCfg", + "env_cfg_entry_point": f"{__name__}.handover_manager_env_cfg:HandoverManagerEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:HandoverPPORunnerCfg", "default_agent": "rsl_rl", - "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", - "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", - "skrl_ippo_cfg_entry_point": f"{agents.__name__}:skrl_ippo_cfg.yaml", - "skrl_mappo_cfg_entry_point": f"{agents.__name__}:skrl_mappo_cfg.yaml", }, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env.py index 7ba3261b6858..37fad7397936 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env.py @@ -3,10 +3,12 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Direct-workflow two-hand handover environment.""" from __future__ import annotations from collections.abc import Sequence +from typing import TYPE_CHECKING import torch @@ -15,7 +17,6 @@ from isaaclab.utils.math import quat_conjugate, quat_mul, sample_uniform, saturate, scale_transform, unscale_transform from isaaclab_tasks.core.handover.handover_common import GOAL_POSITION_OFFSET -from isaaclab_tasks.core.handover.handover_env_cfg import HandoverEnvCfg from isaaclab_tasks.core.handover.mdp.rewards import evaluate_handover_success, handover_reward from isaaclab_tasks.core.reorient.utils import ( EpisodeErrorRecorder, @@ -24,8 +25,16 @@ sample_joint_positions_within_limits, ) +if TYPE_CHECKING: + from isaaclab_tasks.core.handover.handover_env_cfg import HandoverEnvCfg + class HandoverEnv(DirectMARLEnv): + """Two Shadow Hands hand a ball over to a fixed goal position. + + Both agents observe their own hand plus the object and goal, and share one distance reward. + """ + cfg: HandoverEnvCfg def __init__(self, cfg: HandoverEnvCfg, render_mode: str | None = None, **kwargs): @@ -238,8 +247,8 @@ def _get_dones(self) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]: def _reset_idx(self, env_ids: Sequence[int] | torch.Tensor | None): if env_ids is None: env_ids = self.right_hand._ALL_INDICES - # Flush per-episode success: the object is AT the goal as the episode ends, not merely - # that it passed through. 0-dim device tensor, for the same reason. + # flush the per-episode success: the object is at the goal as the episode ends, not merely + # passed through it. Logged as a 0-dim device tensor to avoid a host sync. succeeded = (self._last_goal_dist[env_ids] < self.cfg.success_distance_threshold) & self._episode_succeeded[ env_ids ] @@ -303,7 +312,7 @@ def _reset_idx(self, env_ids: Sequence[int] | torch.Tensor | None): self._compute_intermediate_values() - def _reset_target_pose(self, env_ids): + def _reset_target_pose(self, env_ids: Sequence[int] | torch.Tensor) -> None: # reset goal rotation rand_floats = sample_uniform(-1.0, 1.0, (len(env_ids), 2), device=self.device) new_rot = randomize_rotation( @@ -319,24 +328,22 @@ def _reset_target_pose(self, env_ids): environment_ids=self.scene._ALL_INDICES, ) - def _compute_intermediate_values(self): + def _compute_intermediate_values(self) -> None: # data for right hand - self.right_fingertip_pos = self.right_hand.data.body_pos_w.torch[:, self.finger_bodies] + self.right_fingertip_pos = self.right_hand.data.body_pos_w.torch[ + :, self.finger_bodies + ] - self.scene.env_origins.unsqueeze(1) self.right_fingertip_rot = self.right_hand.data.body_quat_w.torch[:, self.finger_bodies] - self.right_fingertip_pos -= self.scene.env_origins.repeat((1, self.num_fingertips)).reshape( - self.num_envs, self.num_fingertips, 3 - ) self.right_fingertip_velocities = self.right_hand.data.body_vel_w.torch[:, self.finger_bodies] self.right_hand_dof_pos = self.right_hand.data.joint_pos.torch self.right_hand_dof_vel = self.right_hand.data.joint_vel.torch # data for left hand - self.left_fingertip_pos = self.left_hand.data.body_pos_w.torch[:, self.finger_bodies] + self.left_fingertip_pos = self.left_hand.data.body_pos_w.torch[ + :, self.finger_bodies + ] - self.scene.env_origins.unsqueeze(1) self.left_fingertip_rot = self.left_hand.data.body_quat_w.torch[:, self.finger_bodies] - self.left_fingertip_pos -= self.scene.env_origins.repeat((1, self.num_fingertips)).reshape( - self.num_envs, self.num_fingertips, 3 - ) self.left_fingertip_velocities = self.left_hand.data.body_vel_w.torch[:, self.finger_bodies] self.left_hand_dof_pos = self.left_hand.data.joint_pos.torch diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py index 22907774a4bf..d9fd0b2d3efb 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the direct-workflow Shadow Hand handover environment.""" + import torch from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg from isaaclab_ov.physics import OvPhysxCfg @@ -43,24 +45,22 @@ def _hand_cfg( ) -> ArticulationCfg: """Place one engine's Shadow Hand at this task's pose for one hand. - The catch needs more joint authority than reorientation, but the hand's gains belong to the - hand, so both tasks take them as the asset configuration supplies them. This task used to raise - every actuator to stiffness 20 / damping 2, which also drove the tendon-coupled joints -- they - take no position command, and MEASURED, giving them one costs the tendon most of its travel: - 11.1 rad falls to 1.0 rad. + The hand's actuator gains are taken as the asset configuration supplies them. Raising the gains + of every actuator also drives the tendon-coupled joints, which take no position command, and + costs the tendon most of its travel. Args: base: The hand on the engine's asset variant. prim_path: Scene path the hand spawns at. init_pos: Spawn position [m]. - init_rot: Spawn orientation as ``(w, x, y, z)``. + init_rot: Spawn orientation as an ``(x, y, z, w)`` quaternion. Returns: - That configuration at *prim_path* with the given pose. + That configuration at ``prim_path`` with the given pose. """ - # The asset's own spawn rotation is shared by both engines, so the per-hand rotation COMPOSES - # with it rather than replacing it -- replacing leaves both palms turned 90 degrees. See - # SHADOW_HAND_PHYSX_CFG's init_state for why the asset carries that rotation. + # The asset's own spawn rotation is shared by both engines, so the per-hand rotation composes with + # it rather than replacing it; replacing leaves both palms turned 90 degrees. See the init_state + # of SHADOW_HAND_PHYSX_CFG for why the asset carries that rotation. hand_rot = tuple( math_utils.quat_mul( torch.tensor(init_rot, dtype=torch.float64), @@ -73,8 +73,7 @@ def _hand_cfg( ) -# Per-hand poses. The rotations are composed with the asset's own; they are unchanged from the -# previous Newton asset, which the two assets being identical geometry makes valid. +# per-hand poses, composed with the asset's own rotation _RIGHT_POSE = ("{ENV_REGEX_NS}/RightRobot", (0.0, 0.0, 0.5), (0.0, 0.0, 0.0, 1.0)) _LEFT_POSE = ("{ENV_REGEX_NS}/LeftRobot", (0.0, -1.0, 0.5), (0.0, 0.0, 1.0, 0.0)) @@ -178,6 +177,8 @@ class HandoverSceneCfg(InteractiveSceneCfg): @configclass class HandoverEnvCfg(DirectMARLEnvCfg): + """Configuration for the direct-workflow two-hand handover environment.""" + # env decimation = 2 episode_length_s = 7.5 @@ -186,7 +187,7 @@ class HandoverEnvCfg(DirectMARLEnvCfg): observation_spaces = {"right_hand": 157, "left_hand": 157} state_space = 290 - # simulation — values mirrored by the manager cfg + # simulation, mirrored by the manager-based configuration sim: SimulationCfg = SimulationCfg( dt=1 / 120, render_interval=decimation, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py index 22a622f21178..f4f3200846a2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py @@ -17,23 +17,18 @@ from isaaclab.scene import InteractiveSceneCfg from isaaclab.sim.spawners.materials import RigidBodyMaterialBaseCfg from isaaclab.utils import configclass +from isaaclab.visualizers import VisualizerCfg import isaaclab_tasks.core.handover.mdp as mdp import isaaclab_tasks.core.reorient.mdp as reorient_mdp -from isaaclab_tasks.core.handover.handover_env_cfg import ( - BALL_CFG, - LeftHandCfg, - PhysicsCfg, - RightHandCfg, -) +from isaaclab_tasks.core.handover.handover_env_cfg import BALL_CFG, LeftHandCfg, PhysicsCfg, RightHandCfg from isaaclab_tasks.utils import PresetCfg -from isaaclab_assets.robots.shadow_hand import ( - FINGERTIP_NAMES, - JOINT_NAMES, - TENDON_NAMES, - TENDON_POSITION_LIMITS, -) +from isaaclab_assets.robots.shadow_hand import FINGERTIP_NAMES, JOINT_NAMES, TENDON_NAMES, TENDON_POSITION_LIMITS + +## +# Scene definition +## @configclass @@ -57,6 +52,11 @@ class HandoverManagerSceneCfg(InteractiveSceneCfg): ) +## +# MDP settings +## + + @configclass class CommandsCfg: """Handover goal command.""" @@ -64,6 +64,19 @@ class CommandsCfg: object_pose = mdp.HandoverCommandCfg(asset_name="object", success_distance_threshold=0.1, debug_vis=True) +TENDON_ACTION_CFG = mdp.FixedTendonPositionActionCfg( + asset_name="robot", + tendon_names=TENDON_NAMES, + # four of the twenty motors pull a tendon across a finger's middle and distal joints; tendons have + # their own index space, so no joint term can reach them. Map the policy's [-1, 1] onto the + # tendon's commandable span and clip to the bound the task guarantees. + scale=0.5 * (TENDON_POSITION_LIMITS[1] - TENDON_POSITION_LIMITS[0]), + offset=0.5 * (TENDON_POSITION_LIMITS[0] + TENDON_POSITION_LIMITS[1]), + clip={".*": TENDON_POSITION_LIMITS}, +) +"""Tendon position action term of one hand, before the asset name is set.""" + + @configclass class ActionsCfg: """Two-hand action terms, ordered right then left like the Direct adapter. @@ -73,44 +86,20 @@ class ActionsCfg: """ right_hand = mdp.EMAJointPositionToLimitsActionCfg( - asset_name="right_hand", - joint_names=JOINT_NAMES, - alpha=1.0, - rescale_to_limits=True, - ) - right_hand_tendons = mdp.FixedTendonPositionActionCfg( - asset_name="right_hand", - tendon_names=TENDON_NAMES, - # the other four motors pull a tendon across a finger's middle and distal joints; - # tendons have their own index space, so no joint term can reach them. Map the - # policy's [-1, 1] onto the tendon's commandable span. - scale=0.5 * (TENDON_POSITION_LIMITS[1] - TENDON_POSITION_LIMITS[0]), - offset=0.5 * (TENDON_POSITION_LIMITS[0] + TENDON_POSITION_LIMITS[1]), - # the term maps [-1, 1] onto that span; clip states the bound the task guarantees - clip={".*": TENDON_POSITION_LIMITS}, + asset_name="right_hand", joint_names=JOINT_NAMES, alpha=1.0, rescale_to_limits=True ) + right_hand_tendons = TENDON_ACTION_CFG.replace(asset_name="right_hand") left_hand = mdp.EMAJointPositionToLimitsActionCfg( - asset_name="left_hand", - joint_names=JOINT_NAMES, - alpha=1.0, - rescale_to_limits=True, - ) - left_hand_tendons = mdp.FixedTendonPositionActionCfg( - asset_name="left_hand", - tendon_names=TENDON_NAMES, - # the other four motors pull a tendon across a finger's middle and distal joints; - # tendons have their own index space, so no joint term can reach them. Map the - # policy's [-1, 1] onto the tendon's commandable span. - scale=0.5 * (TENDON_POSITION_LIMITS[1] - TENDON_POSITION_LIMITS[0]), - offset=0.5 * (TENDON_POSITION_LIMITS[0] + TENDON_POSITION_LIMITS[1]), - # the term maps [-1, 1] onto that span; clip states the bound the task guarantees - clip={".*": TENDON_POSITION_LIMITS}, + asset_name="left_hand", joint_names=JOINT_NAMES, alpha=1.0, rescale_to_limits=True ) + left_hand_tendons = TENDON_ACTION_CFG.replace(asset_name="left_hand") @configclass class PolicyCfg(ObsGroup): - # Right agent: 133 hand dimensions followed by 24 object/goal dimensions. + """Both hands' proprioception plus the object and goal state, in the Direct adapter's order.""" + + # right agent: 133 hand dimensions followed by 24 object/goal dimensions # soft limits equal the hard limits here: soft_joint_pos_limits_factor defaults to 1.0 right_joint_pos = ObsTerm( func=mdp.joint_pos_limit_normalized, params={"asset_cfg": SceneEntityCfg("right_hand", joint_names=".*")} @@ -125,10 +114,10 @@ class PolicyCfg(ObsGroup): func=reorient_mdp.fingertip_vel, params={"asset_cfg": SceneEntityCfg("right_hand", body_names=FINGERTIP_NAMES)}, ) - right_action = ObsTerm(func=mdp.last_action, params={"action_name": "right_hand"}) - # A hand's motors span two action terms, so its previous command does too: without the tendon + # a hand's motors span two action terms, so its previous command does too: without the tendon # term the policy sees 16 of the 20 actions it took, and the group falls 4 short of the 133 - # hand dimensions the Direct task lays out. + # hand dimensions the Direct task lays out + right_action = ObsTerm(func=mdp.last_action, params={"action_name": "right_hand"}) right_tendon_action = ObsTerm(func=mdp.last_action, params={"action_name": "right_hand_tendons"}) object_pos = ObsTerm(func=mdp.root_pos_w, params={"asset_cfg": SceneEntityCfg("object")}) object_quat = ObsTerm(func=mdp.root_quat_w, params={"asset_cfg": SceneEntityCfg("object")}) @@ -140,8 +129,7 @@ class PolicyCfg(ObsGroup): params={"asset_cfg": SceneEntityCfg("object"), "command_name": "object_pose", "make_quat_unique": False}, ) - # Left agent: the same 157-dimensional layout. - # soft limits equal the hard limits here: soft_joint_pos_limits_factor defaults to 1.0 + # left agent: the same 157-dimensional layout left_joint_pos = ObsTerm( func=mdp.joint_pos_limit_normalized, params={"asset_cfg": SceneEntityCfg("left_hand", joint_names=".*")} ) @@ -302,24 +290,38 @@ class TerminationsCfg: time_out = DoneTerm(func=mdp.time_out, time_out=True) +## +# Environment configuration +## + + @configclass class HandoverManagerEnvCfg(ManagerBasedRLEnvCfg): """Manager-based handover environment matching the Direct RSL-RL view.""" + # Scene settings scene: HandoverManagerSceneCfg = HandoverManagerSceneCfg() + # Basic settings observations: ObservationsCfg = ObservationsCfg() actions: ActionsCfg = ActionsCfg() commands: CommandsCfg = CommandsCfg() + # MDP settings rewards: RewardsCfg = RewardsCfg() terminations: TerminationsCfg = TerminationsCfg() + # ``presets=randomized`` adds the domain-randomization terms events: HandoverEventPresetCfg = HandoverEventPresetCfg() def __post_init__(self): + """Post initialization.""" + # general settings self.decimation = 2 self.episode_length_s = 7.5 - # simulation — mirrors the Direct cfg + # simulation settings, mirrored from the Direct configuration self.sim.dt = 1 / 120 self.sim.render_interval = self.decimation self.sim.physics_material = RigidBodyMaterialBaseCfg(static_friction=1.0, dynamic_friction=1.0) self.sim.physics = PhysicsCfg() - self.viewer.eye = (2.0, 2.0, 2.0) + # visualizer settings: frame both hands and the object between them + self.sim.default_visualizer_cfg = VisualizerCfg( + eye=(1.15, -1.65, 1.15), lookat=(0.0, -0.5, 0.55), focal_length=35.0 + ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/__init__.py index 3a693209a889..680495dd3b8c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/__init__.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Manager-based MDP terms for the Shadow Hand handover task.""" +"""MDP terms specific to the Shadow Hand handover environments.""" from isaaclab.utils.module import lazy_export diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands.py index 95d8e2eae73f..5d070e778675 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands.py @@ -59,10 +59,9 @@ def _update_metrics(self) -> None: def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: if env_ids is None: env_ids = slice(None) - # the base class means the metric over ``env_ids``, converts it to a float and - # zeroes it, so the episode's success bit is written before delegating - # Success is the object being AT the goal when the episode ends, not having passed - # through it. The latch guards the first reset, before any distance is measured. + # The base class averages the metric over ``env_ids`` and zeroes it, so the episode's success bit is + # written before delegating. Success means the object is at the goal when the episode ends, not that + # it passed through it; the latch guards the first reset, before any distance is measured. self.metrics["success_rate"][env_ids] = ( (self.metrics["goal_distance"][env_ids] < self.cfg.success_distance_threshold) & self._succeeded[env_ids] ).float() @@ -78,8 +77,7 @@ def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: return extras def _resample_command(self, env_ids: Sequence[int]) -> None: - # The shared sampler covers SO(3) uniformly. Composing two axis-angle rotations, as this did, - # reaches only a two-axis subset and needs a unit-axis buffer per axis to do it. + # sample uniformly over SO(3) rather than composing single-axis rotations, which only reaches a subset self.quat_command_w[env_ids] = math_utils.random_orientation(len(env_ids), device=self.device) def _update_command(self) -> None: diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/__init__.py index 5a87d61ecb08..a42cb372edf3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/__init__.py @@ -32,6 +32,4 @@ year={2026}, url={https://arxiv.org/abs/2603.15789}, } - - """ diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/adr_curriculum.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/adr_curriculum.py index 842dffe11802..f9f016a268ae 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/adr_curriculum.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/adr_curriculum.py @@ -3,32 +3,27 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Adaptive domain randomization curriculum for the lift environments.""" + from isaaclab.managers import CurriculumTermCfg as CurrTerm from isaaclab.utils import configclass -from . import mdp +import isaaclab_tasks.core.lift.mdp as mdp @configclass class CurriculumCfg: - """Curriculum terms for the MDP.""" + """Curriculum terms for the MDP. + + The ``adr`` (adaptive domain randomization) scheduler promotes each environment's difficulty on + success; the remaining terms interpolate observation noise, termination bounds and gravity between + their initial and final values as the mean difficulty grows. + """ - # adr stands for automatic/adaptive domain randomization adr = CurrTerm( func=mdp.DifficultyScheduler, params={"init_difficulty": 0, "min_difficulty": 0, "max_difficulty": 10} ) - def disable_observation_noise_terms(self) -> None: - """Drop the terms that schedule observation noise. - - Required whenever observation corruption is off: the observation manager clears the ``noise`` - configuration of every term in an uncorrupted group, so the addresses these terms interpolate - no longer resolve and the first curriculum evaluation would raise ``AttributeError``. - """ - for term_name, term in list(self.__dict__.items()): - if term is not None and ".noise." in term.params.get("address", ""): - setattr(self, term_name, None) - joint_pos_unoise_min_adr = CurrTerm( func=mdp.modify_term_cfg, params={ @@ -115,7 +110,7 @@ def disable_observation_noise_terms(self) -> None: params={ "address": "observations.perception.object_point_cloud.noise.n_max", "modify_fn": mdp.initial_final_interpolate_fn, - "modify_params": {"initial_value": 0.0, "final_value": -0.01, "difficulty_term_str": "adr"}, + "modify_params": {"initial_value": 0.0, "final_value": 0.01, "difficulty_term_str": "adr"}, }, ) @@ -140,3 +135,14 @@ def disable_observation_noise_terms(self) -> None: }, }, ) + + def disable_observation_noise_terms(self) -> None: + """Drop the terms that schedule observation noise. + + Required whenever observation corruption is off: the observation manager clears the ``noise`` + configuration of every term in an uncorrupted group, so the addresses these terms interpolate + no longer resolve and the first curriculum evaluation would raise ``AttributeError``. + """ + for term_name, term in list(self.__dict__.items()): + if term is not None and ".noise." in term.params.get("address", ""): + setattr(self, term_name, None) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka/franka_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka/franka_env_cfg.py index b244c84dcab0..06c1afa57bce 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka/franka_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka/franka_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the Franka lift and reorient environments.""" + from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg from isaaclab.managers import EventTermCfg as EventTerm @@ -14,22 +16,24 @@ from isaaclab.utils import configclass from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR +import isaaclab_tasks.core.lift.lift_env_cfg as lift +import isaaclab_tasks.core.lift.mdp as mdp + from isaaclab_assets.robots import FRANKA_PANDA_CFG -from ... import lift_env_cfg as lift -from ... import mdp +## +# Scene assets +## -# Lift runs the menagerie-converted asset (identified inertials, authored finger -# coupling) with actuators calibrated for it; the stock FRANKA_PANDA_CFG stays on the -# legacy asset so the upstream franka tasks keep their demos and baselines. +# The lift tasks run the menagerie-converted asset (identified inertials, authored finger coupling) with +# actuators calibrated for it, while the other Franka tasks keep the stock asset. FRANKA_PANDA_LIFT_CFG = FRANKA_PANDA_CFG.copy() FRANKA_PANDA_LIFT_CFG.spawn.usd_path = f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/franka_panda.usda" # Reset clearance was calibrated for these arm meshes; the asset's primitive colliders intersect the ground. FRANKA_PANDA_LIFT_CFG.spawn.variants = {"Colliders": "convex_hulls"} FRANKA_PANDA_LIFT_CFG.actuators = { - # Inspired by libfranka's joint_impedance_control.cpp. ``actuator_velocity_limit`` - # remains the soft task-limit snapshot; ``joint_velocity_limit`` is the - # separate solver request. + # inspired by libfranka's joint_impedance_control.cpp; ``actuator_velocity_limit`` is the soft task + # limit and ``joint_velocity_limit`` the separate solver request "panda_arm": ImplicitActuatorCfg( joint_names_expr=["panda_joint[1-7]"], joint_effort_limit={"panda_joint[1-4]": 87.0, "panda_joint[5-7]": 12.0}, @@ -73,26 +77,35 @@ ), } +"""Franka Panda configuration for the lift tasks.""" + FINGERTIP_LIST = ["panda_rightfinger", "panda_leftfinger"] +"""Finger bodies that carry an object contact sensor.""" + THUMB_SENSOR = "panda_leftfinger_object_s" -FINGER_SENSORS = [f"{name}_object_s" for name in FINGERTIP_LIST if name != THUMB_SENSOR.replace("_object_s", "")] +"""Contact sensor that plays the thumb in the finger-contact rewards.""" + +FINGER_SENSORS = [f"{name}_object_s" for name in FINGERTIP_LIST if name != "panda_leftfinger"] +"""Contact sensors of the remaining fingers.""" + + +## +# Scene definition +## @configclass class FrankaSceneCfg(lift.SceneCfg): - """Franka scene for the Lift and Reorient tasks.""" + """Franka scene for the lift and reorient tasks.""" robot: ArticulationCfg = FRANKA_PANDA_LIFT_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") def __post_init__(self): super().__post_init__() self.robot.spawn.activate_contact_sensors = True - # the converted menagerie asset already authors the finger-coupling mimic in its - # physics payload; re-enable _spawn_franka_with_finger_equality only for assets - # that lack it (e.g. panda_instanceable) + # the base is rotated by 180 degrees about z so the workspace lies at positive x self.robot.init_state.rot = (0.0, 0.0, 1.0, 0.0) - # keep action targets off the hard stops: the policy must not learn to ride - # joint limits (a compliant-limit affordance that does not transfer across engines) + # one object contact sensor per finger for link_name in FINGERTIP_LIST: setattr( self, @@ -117,6 +130,11 @@ def __post_init__(self): self.object.spawn.default.assets_cfg = graspable_shape_assets_cfg +## +# MDP settings +## + + @configclass class StateObservationCfg(lift.ObservationsCfg): """State observations for the Franka lift tasks.""" @@ -133,11 +151,15 @@ def __post_init__(self): @configclass class FrankaRelJointPosActionCfg: + """Relative joint position targets for all joints.""" + action = mdp.RelativeJointPositionActionCfg(asset_name="robot", joint_names=[".*"], scale=0.1) @configclass class FrankaReorientRewardCfg(lift.RewardsCfg): + """Reward terms for the MDP, with the Franka finger contact sensors filled in.""" + good_finger_contact = RewTerm( func=mdp.contacts, weight=0.75, @@ -186,6 +208,7 @@ class FrankaEventCfg(lift.EventCfg): ) def __post_init__(self): + super().__post_init__() reset_terms = self.conditional_reset.params["terms"] criteria = self.conditional_reset.params["valid_criteria"] # the coupled finger pair is one mechanical DOF: independent per-joint draws write @@ -194,7 +217,7 @@ def __post_init__(self): reset_terms["reset_robot_joints"].params["asset_cfg"] = SceneEntityCfg("robot", joint_names="panda_joint.*") fingers = SceneEntityCfg("robot", joint_names="panda_finger_joint.*") reset_terms["reset_gripper_width"] = EventTerm( - func="isaaclab_tasks.core.lift.mdp.events:reset_joints_shared_offset", + func=mdp.reset_joints_shared_offset, mode="reset", params={"position_range": [-0.04, 0.0], "asset_cfg": fingers}, ) @@ -213,15 +236,22 @@ def __post_init__(self): self.joint_stiffness_and_damping.params["asset_cfg"] = SceneEntityCfg("robot", joint_names="panda_joint.*") +## +# Environment configuration +## + + @configclass class FrankaMixinCfg: + """Franka-specific scene, observation, action, reward and event terms, mixed into the task configurations.""" + scene: FrankaSceneCfg = FrankaSceneCfg(num_envs=4096, env_spacing=3, replicate_physics=True) rewards: FrankaReorientRewardCfg = FrankaReorientRewardCfg() observations: StateObservationCfg = StateObservationCfg() actions: FrankaRelJointPosActionCfg = FrankaRelJointPosActionCfg() events: FrankaEventCfg = FrankaEventCfg() - def __post_init__(self: lift.ReorientEnvCfg): + def __post_init__(self): super().__post_init__() self.commands.object_pose.body_name = "panda_hand" # Franka base is rotated 180 deg about z, so the workspace mirrors to positive x. @@ -231,21 +261,21 @@ def __post_init__(self: lift.ReorientEnvCfg): @configclass class FrankaReorientEnvCfg(FrankaMixinCfg, lift.ReorientEnvCfg): + """Franka object reorientation environment.""" + def play_mode(self): - # play-mode overrides of parent super().play_mode() - - # deploy/eval at the datasheet gripper speed: no closing-speed randomization, and - # the hand kd=175 caps closing at 0.2 m/s (the real hand's jaw-speed limit) + # evaluate at the datasheet gripper speed: without the closing-speed randomization the hand + # damping caps closing at the real hand's jaw-speed limit of 0.2 m/s self.events.gripper_closing_speed = None @configclass class FrankaLiftEnvCfg(FrankaMixinCfg, lift.LiftEnvCfg): + """Franka object lifting environment.""" + def play_mode(self): - # play-mode overrides of parent super().play_mode() - - # deploy/eval at the datasheet gripper speed: no closing-speed randomization, and - # the hand kd=175 caps closing at 0.2 m/s (the real hand's jaw-speed limit) + # evaluate at the datasheet gripper speed: without the closing-speed randomization the hand + # damping caps closing at the real hand's jaw-speed limit of 0.2 m/s self.events.gripper_closing_speed = None diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/__init__.py index 5f952eee37df..d6be1fd260c9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/__init__.py @@ -3,9 +3,11 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Franka deformable, cloth, and cable lifting environments.""" + import gymnasium as gym -from isaaclab_tasks.core.lift.config.franka_soft import agents +from . import agents ## # Register Gym environments. @@ -33,7 +35,6 @@ }, ) - gym.register( id="Isaac-Lift-Cable-Franka", entry_point="isaaclab.envs:ManagerBasedRLEnv", @@ -41,10 +42,10 @@ kwargs={ "env_cfg_entry_point": f"{__name__}.franka_cable_env_cfg:FrankaCableEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:FrankaCablePPORunnerCfg", + "default_agent": "rsl_rl", }, ) - gym.register( id="Isaac-Lift-Cable-Franka-Camera", entry_point="isaaclab.envs:ManagerBasedRLEnv", @@ -52,10 +53,10 @@ kwargs={ "env_cfg_entry_point": f"{__name__}.franka_cable_env_cfg:FrankaCableCameraEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:FrankaCableCameraPPORunnerCfg", + "default_agent": "rsl_rl", }, ) - gym.register( id="Isaac-Lift-Soft-Franka-Camera", entry_point="isaaclab.envs:ManagerBasedRLEnv", diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cable_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cable_env_cfg.py index 6f310ae40bff..c4a859463b41 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cable_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cable_env_cfg.py @@ -25,18 +25,10 @@ from isaaclab_contrib.coupling import CouplerEntryCfg, CouplerProxyCfg, CouplerProxyMappingCfg +import isaaclab_tasks.core.lift.mdp as mdp +from isaaclab_tasks.core.lift.config.franka_soft import franka_soft_env_cfg as soft from isaaclab_tasks.utils import PresetCfg -from ... import mdp -from .franka_soft_env_cfg import ( - FRANKA_CAMERA_CFG, - TABLE_SPAWN_CFG, - FrankaCameraObservationsCfg, - FrankaSoftEnvCfg, - _FrankaSoftSceneCfg, -) -from .franka_soft_env_cfg import EventCfg as FrankaSoftEventCfg - _CABLE_SEGMENT_COUNT = 12 _CABLE_MIDDLE_SEGMENT_INDEX = _CABLE_SEGMENT_COUNT // 2 @@ -54,9 +46,14 @@ ) +## +# Physics backend presets +## + + @configclass class PhysicsCfg(PresetCfg): - """Newton proxy physics for rigid-cable coupling.""" + """Physics backend presets for the cable environment: Newton proxy physics for rigid-cable coupling.""" newton_mjwarp_vbd_proxy: NewtonCfg = NewtonCfg( solver_cfg=CouplerProxyCfg( @@ -101,8 +98,13 @@ class PhysicsCfg(PresetCfg): default = newton_mjwarp_vbd_proxy +## +# Scene definition +## + + @configclass -class FrankaCableSceneCfg(_FrankaSoftSceneCfg): +class FrankaCableSceneCfg(soft.FrankaSoftBaseSceneCfg): """Scene for the Franka cable lifting environment.""" deformable: None = None @@ -110,7 +112,7 @@ class FrankaCableSceneCfg(_FrankaSoftSceneCfg): table: AssetBaseCfg = AssetBaseCfg( prim_path="{ENV_REGEX_NS}/Table", init_state=AssetBaseCfg.InitialStateCfg(pos=[0.5, 0.0, -0.525]), - spawn=TABLE_SPAWN_CFG.replace( + spawn=soft.TABLE_SPAWN_CFG.replace( physics_material=RigidBodyMaterialBaseCfg(static_friction=0.01, dynamic_friction=0.01), ), ) @@ -153,12 +155,17 @@ class FrankaCableSceneCfg(_FrankaSoftSceneCfg): class FrankaCableCameraSceneCfg(FrankaCableSceneCfg): """Franka cable scene with a base camera.""" - base_camera: CameraCfg = FRANKA_CAMERA_CFG + base_camera: CameraCfg = soft.FRANKA_CAMERA_CFG + + +## +# MDP settings +## @configclass class CommandsCfg: - """Goal position for cable segment 6 in the robot root frame.""" + """Goal position of the middle cable segment in the robot root frame.""" cable_pose = mdp.CableUniformPoseCommandCfg( asset_name="robot", @@ -178,10 +185,10 @@ class CommandsCfg: success_visualizer_cfg=VisualizationMarkersCfg( prim_path="/Visuals/SuccessMarkers", markers={ - "failure": TABLE_SPAWN_CFG.replace( + "failure": soft.TABLE_SPAWN_CFG.replace( visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.8, 0.5, 0.5)), visible=True ), - "success": TABLE_SPAWN_CFG.replace( + "success": soft.TABLE_SPAWN_CFG.replace( visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.5, 0.8, 0.5)), visible=True ), }, @@ -195,6 +202,8 @@ class ObservationsCfg: @configclass class PolicyCfg(ObsGroup): + """Observations for policy group.""" + joint_pos = ObsTerm(func=mdp.joint_pos_rel) joint_vel = ObsTerm(func=mdp.joint_vel_rel) cable_segment_positions = ObsTerm( @@ -204,7 +213,7 @@ class PolicyCfg(ObsGroup): target_position = ObsTerm(func=mdp.generated_commands, params={"command_name": "cable_pose"}) actions = ObsTerm(func=mdp.last_action) - def __post_init__(self) -> None: + def __post_init__(self): self.enable_corruption = True self.concatenate_terms = True @@ -212,21 +221,25 @@ def __post_init__(self) -> None: @configclass -class FrankaCableCameraObservationsCfg(FrankaCameraObservationsCfg): +class FrankaCableCameraObservationsCfg(soft.FrankaCameraObservationsCfg): """Observation groups for visual cable lifting.""" @configclass - class PolicyCfg(FrankaCameraObservationsCfg.PolicyCfg): + class PolicyCfg(soft.FrankaCameraObservationsCfg.PolicyCfg): + """Observations for policy group.""" + target_position = ObsTerm(func=mdp.generated_commands, params={"command_name": "cable_pose"}) @configclass class PerceptionCfg(ObsGroup): + """Observations for perception group.""" + cable_segment_positions = ObsTerm( func=mdp.cable_segment_positions_in_robot_root_frame, params={"asset_cfg": SceneEntityCfg("cable")}, ) - def __post_init__(self) -> None: + def __post_init__(self): self.enable_corruption = True self.concatenate_terms = True @@ -235,13 +248,13 @@ def __post_init__(self) -> None: @configclass -class EventCfg(FrankaSoftEventCfg): +class EventCfg(soft.EventCfg): """Reset events for the Franka cable environment.""" reset_deformable: EventTerm | None = None reset_cable = EventTerm( - func="isaaclab_tasks.core.lift.mdp.events:reset_cable_state_uniform", + func=mdp.reset_cable_state_uniform, mode="reset", params={ "position_range": {"x": (-0.15, 0.1), "y": (-0.2, 0.2), "z": (0.0, 0.0)}, @@ -326,8 +339,13 @@ class TerminationsCfg: ) +## +# Environment configuration +## + + @configclass -class FrankaCableEnvCfg(FrankaSoftEnvCfg): +class FrankaCableEnvCfg(soft.FrankaSoftEnvCfg): """Manager-based RL environment for lifting a 12-segment cable.""" scene: FrankaCableSceneCfg = FrankaCableSceneCfg(num_envs=8192, env_spacing=2.0, replicate_physics=True) @@ -337,14 +355,14 @@ class FrankaCableEnvCfg(FrankaSoftEnvCfg): terminations: TerminationsCfg = TerminationsCfg() events: EventCfg = EventCfg() - def __post_init__(self) -> None: + def __post_init__(self): super().__post_init__() self.sim.physics = PhysicsCfg() - # Close the gripper on the thin cable; the shared beam default only closes to 0.01 m. + # fully close the gripper on the thin cable; the shared beam default only closes to 0.01 m self.actions.ik.gripper_action.close_command_expr = {"panda_finger_joint1": 0.0} - # Only a partitioned Kit RTX render can cull the cable. The check mirrors the default that - # IsaacRtxRendererCfg.enable_scene_partitioning resolves to; assigning that field explicitly - # bypasses it, so re-add the markers by hand in that case. + # only a partitioned Kit RTX render can cull the cable; the check mirrors the default that + # IsaacRtxRendererCfg.enable_scene_partitioning resolves to, so an explicit assignment of that + # field must re-add the markers by hand if not isaac_rtx_per_env_scene_partition_enabled(): self.scene.partition_bounds_marker_min = None self.scene.partition_bounds_marker_max = None @@ -357,6 +375,6 @@ class FrankaCableCameraEnvCfg(FrankaCableEnvCfg): scene: FrankaCableCameraSceneCfg = FrankaCableCameraSceneCfg(num_envs=128, env_spacing=2.0, replicate_physics=True) observations: FrankaCableCameraObservationsCfg = FrankaCableCameraObservationsCfg() - def __post_init__(self) -> None: + def __post_init__(self): super().__post_init__() self.num_rerenders_on_reset = 2 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py index 5c3161773c2c..8e0f552c6b33 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py @@ -32,29 +32,19 @@ from isaaclab_contrib.coupling import CouplerEntryCfg, CouplerProxyCfg, CouplerProxyMappingCfg +import isaaclab_tasks.core.lift.mdp as mdp +from isaaclab_tasks.core.lift.config.franka_soft import franka_soft_env_cfg as soft from isaaclab_tasks.utils import PresetCfg -from ... import mdp -from .franka_soft_env_cfg import ( - FRANKA_CAMERA_CFG, - FrankaCameraObservationsCfg, - FrankaSoftEnvCfg, - _FrankaSoftSceneCfg, -) -from .franka_soft_env_cfg import ( - EventCfg as FrankaSoftEventCfg, -) -from .franka_soft_env_cfg import ( - RewardsCfg as FrankaSoftRewardsCfg, -) - ## -# Scene definition +# Physics backend presets ## @configclass class PhysicsCfg(PresetCfg): + """Physics backend presets for the cloth environment.""" + newton_mjwarp_vbd_proxy: NewtonCfg = NewtonCfg( solver_cfg=CouplerProxyCfg( entries=[ @@ -106,6 +96,10 @@ class PhysicsCfg(PresetCfg): default = newton_mjwarp_vbd_proxy +## +# Scene definition +## + SUPPORT_SPAWN_CFG = sim_utils.CuboidCfg( size=(0.1, 0.02, 0.15), rigid_props=[sim_utils.UsdPhysicsRigidBodyCfg(kinematic_enabled=True), PhysxRigidBodyCfg(disable_gravity=True)], @@ -114,11 +108,12 @@ class PhysicsCfg(PresetCfg): physics_material=sim_utils.RigidBodyMaterialCfg(static_friction=0.01, dynamic_friction=0.01), visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.2, 0.2, 0.25)), ) +"""Kinematic support block the cloth is draped over.""" @configclass class DeformableCfg(PresetCfg): - """Preset configurations for the cloth.""" + """Cloth presets per physics backend.""" newton_mjwarp_vbd_proxy: DeformableObjectCfg = DeformableObjectCfg( prim_path="{ENV_REGEX_NS}/Deformable", @@ -168,7 +163,7 @@ class DeformableCfg(PresetCfg): @configclass -class FrankaClothSceneCfg(_FrankaSoftSceneCfg): +class FrankaClothSceneCfg(soft.FrankaSoftBaseSceneCfg): """Scene for the Franka surface deformable environment.""" deformable: DeformableCfg = DeformableCfg() @@ -184,7 +179,7 @@ class FrankaClothSceneCfg(_FrankaSoftSceneCfg): spawn=SUPPORT_SPAWN_CFG, ) - def __post_init__(self) -> None: + def __post_init__(self): super().__post_init__() # increase franka gripper stiffness @@ -212,7 +207,7 @@ class FrankaClothScenePresetCfg(PresetCfg): class FrankaClothCameraSceneCfg(FrankaClothSceneCfg): """Franka cloth scene with a base camera.""" - base_camera: CameraCfg = FRANKA_CAMERA_CFG + base_camera: CameraCfg = soft.FRANKA_CAMERA_CFG @configclass @@ -227,8 +222,13 @@ class FrankaClothCameraScenePresetCfg(PresetCfg): default = newton_mjwarp_vbd_proxy +## +# MDP settings +## + + @configclass -class FrankaClothEventCfg(FrankaSoftEventCfg): +class FrankaClothEventCfg(soft.EventCfg): """Reset and startup events for the Franka cloth environment.""" reset_deformable = EventTerm( @@ -243,13 +243,8 @@ class FrankaClothEventCfg(FrankaSoftEventCfg): ) -## -# Environment configuration -## - - @configclass -class FrankaClothRewardsCfg(FrankaSoftRewardsCfg): +class FrankaClothRewardsCfg(soft.RewardsCfg): """Rewards for the Franka cloth environment.""" reaching_deformable = RewTerm( @@ -265,15 +260,20 @@ class FrankaClothRewardsCfg(FrankaSoftRewardsCfg): ) +## +# Environment configuration +## + + @configclass -class FrankaClothEnvCfg(FrankaSoftEnvCfg): +class FrankaClothEnvCfg(soft.FrankaSoftEnvCfg): """Manager-based RL environment: Franka Panda lifting a surface deformable.""" scene: FrankaClothScenePresetCfg = FrankaClothScenePresetCfg() events: FrankaClothEventCfg = FrankaClothEventCfg() rewards: FrankaClothRewardsCfg = FrankaClothRewardsCfg() - def __post_init__(self) -> None: + def __post_init__(self): super().__post_init__() # override the soft-beam physics with the cloth presets self.sim.physics = PhysicsCfg() @@ -286,9 +286,9 @@ class FrankaClothCameraEnvCfg(FrankaClothEnvCfg): """Visual Franka surface-deformable lifting environment.""" scene: FrankaClothCameraScenePresetCfg = FrankaClothCameraScenePresetCfg() - observations: FrankaCameraObservationsCfg = FrankaCameraObservationsCfg() + observations: soft.FrankaCameraObservationsCfg = soft.FrankaCameraObservationsCfg() - def __post_init__(self) -> None: + def __post_init__(self): super().__post_init__() - # Warm up the RTX render product/annotator (Newton skips the PhysX assets_loading render loop). + # warm up the RTX render product and annotator; Newton skips the PhysX asset-loading render loop self.num_rerenders_on_reset = 2 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py index 4fc5d7fb6413..97dc870b4fbe 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py @@ -26,7 +26,6 @@ from isaaclab.assets.deformable_object import DeformableObjectCfg from isaaclab.controllers import DifferentialIKControllerCfg from isaaclab.envs import ManagerBasedRLEnvCfg -from isaaclab.envs import mdp as env_mdp from isaaclab.managers import CurriculumTermCfg as CurrTerm from isaaclab.managers import EventTermCfg as EventTerm from isaaclab.managers import ObservationGroupCfg as ObsGroup @@ -44,41 +43,28 @@ from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR from isaaclab.visualizers import VisualizerCfg -from isaaclab_contrib.coupling import ( - CouplerEntryCfg, - CouplerProxyCfg, - CouplerProxyMappingCfg, -) +from isaaclab_contrib.coupling import CouplerEntryCfg, CouplerProxyCfg, CouplerProxyMappingCfg +import isaaclab_tasks.core.lift.mdp as mdp from isaaclab_tasks.utils import PresetCfg, preset from isaaclab_tasks.utils.presets import MultiBackendRendererCfg -from ... import mdp - -## -# Pre-defined configs -## - -from isaaclab_assets.robots.franka import FRANKA_PANDA_MENAGERIE_CFG # isort:skip - +from isaaclab_assets.robots.franka import FRANKA_PANDA_MENAGERIE_CFG ## -# Helpers +# Scene assets ## - -# Shared volume material parameters. The Newton config below uses the equivalent Lame parameters. +# shared volume material parameters; the Newton configuration uses the equivalent Lame parameters YOUNGS_MODULUS = 2e5 POISSONS_RATIO = 0.3 -# Table collider whose top surface sits at z = 0. Spawned invisible: the command term's success -# visualizer draws it instead, tinted by whether the goal is reached. TABLE_SPAWN_CFG = sim_utils.CuboidCfg( size=(1.3, 0.9, 1.05), collision_props=sim_utils.UsdPhysicsCollisionCfg(), visible=False, ) - +"""Table collider whose top surface sits at z = 0, drawn by the command term's success markers.""" FRANKA_CAMERA_CFG = CameraCfg( prim_path="{ENV_REGEX_NS}/Camera", @@ -93,11 +79,12 @@ height=128, renderer_cfg=MultiBackendRendererCfg(), ) +"""Base-mounted RGB camera of the visual variants.""" @configclass class DeformableCfg(PresetCfg): - """Preset config for the deformable object, matching the Newton example.""" + """Deformable soft-beam presets per physics backend, matching the Newton example.""" newton_mjwarp_vbd_proxy: DeformableObjectCfg = DeformableObjectCfg( prim_path="{ENV_REGEX_NS}/Deformable", @@ -141,6 +128,8 @@ class DeformableCfg(PresetCfg): @configclass class PhysicsCfg(PresetCfg): + """Physics backend presets for the soft-beam environment.""" + newton_mjwarp_vbd_proxy: NewtonCfg = NewtonCfg( solver_cfg=CouplerProxyCfg( entries=[ @@ -197,8 +186,8 @@ class PhysicsCfg(PresetCfg): @configclass -class _FrankaSoftSceneCfg(InteractiveSceneCfg): - """Scene for the Franka deformable environment.""" +class FrankaSoftBaseSceneCfg(InteractiveSceneCfg): + """Scene for the Franka deformable environment, also the base of the cloth and cable scenes.""" robot: ArticulationCfg = FRANKA_PANDA_MENAGERIE_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") robot.spawn.variants = preset( @@ -225,9 +214,7 @@ class _FrankaSoftSceneCfg(InteractiveSceneCfg): deformable: DeformableCfg = DeformableCfg() - # static table collider with its top surface at z = 0. Kept invisible: the success - # visualizer renders the visible table, colored by whether the goal is reached - # (see CommandsCfg). + # static table collider drawn by the command term's success markers (see CommandsCfg) table: AssetBaseCfg = AssetBaseCfg( prim_path="{ENV_REGEX_NS}/Table", init_state=AssetBaseCfg.InitialStateCfg(pos=[0.5, 0.0, -0.525]), @@ -250,7 +237,7 @@ class _FrankaSoftSceneCfg(InteractiveSceneCfg): ), ) - def __post_init__(self) -> None: + def __post_init__(self): self.robot.actuators = { # inspired by libfranka's joint_impedance_control.cpp "panda_arm": ImplicitActuatorCfg( @@ -295,8 +282,8 @@ def __post_init__(self) -> None: ), } - # disable gravity on the arm so the low-PD actuators do not need to fight gravity sag, - # which is the dominant source of steady-state IK tracking error. + # disable gravity on the arm so the low-gain actuators do not fight gravity sag, the dominant + # source of steady-state IK tracking error self.robot.spawn.rigid_props.disable_gravity = True # increase franka gripper stiffness @@ -306,7 +293,7 @@ def __post_init__(self) -> None: @configclass -class _FrankaSoftCameraSceneCfg(_FrankaSoftSceneCfg): +class FrankaSoftBaseCameraSceneCfg(FrankaSoftBaseSceneCfg): """Franka soft scene with a base camera.""" base_camera: CameraCfg = FRANKA_CAMERA_CFG @@ -403,6 +390,8 @@ class ObservationsCfg: @configclass class PolicyCfg(ObsGroup): + """Observations for policy group.""" + joint_pos = ObsTerm(func=mdp.joint_pos_rel) joint_vel = ObsTerm(func=mdp.joint_vel_rel) deformable_sampled_points = ObsTerm( @@ -412,7 +401,7 @@ class PolicyCfg(ObsGroup): target_position = ObsTerm(func=mdp.generated_commands, params={"command_name": "deformable_pose"}) actions = ObsTerm(func=mdp.last_action) - def __post_init__(self) -> None: + def __post_init__(self): self.enable_corruption = True self.concatenate_terms = True @@ -425,37 +414,45 @@ class FrankaCameraObservationsCfg: @configclass class PolicyCfg(ObsGroup): + """Observations for policy group.""" + target_position = ObsTerm(func=mdp.generated_commands, params={"command_name": "deformable_pose"}) actions = ObsTerm(func=mdp.last_action) - def __post_init__(self) -> None: + def __post_init__(self): self.enable_corruption = True self.concatenate_terms = True @configclass class ProprioCfg(ObsGroup): + """Observations for proprioception group.""" + joint_pos = ObsTerm(func=mdp.joint_pos_rel) joint_vel = ObsTerm(func=mdp.joint_vel_rel) - def __post_init__(self) -> None: + def __post_init__(self): self.enable_corruption = True self.concatenate_terms = True @configclass class PerceptionCfg(ObsGroup): + """Observations for perception group.""" + deformable_sampled_points = ObsTerm( func=mdp.DeformableSampledPointsInRobotRootFrame, params={"asset_cfg": SceneEntityCfg("deformable"), "num_points": 20}, ) - def __post_init__(self) -> None: + def __post_init__(self): self.enable_corruption = True self.concatenate_terms = True @configclass class BaseImageCfg(ObsGroup): + """Camera observations for the base image group.""" + image = ObsTerm( - func=env_mdp.image, + func=mdp.image, params={ "sensor_cfg": SceneEntityCfg("base_camera"), "data_type": "rgb", @@ -563,7 +560,7 @@ class CurriculumCfg: func=mdp.modify_reward_weight, params={"term_name": "action_rate", "weight": -1e-1, "num_steps": 15000} ) - # Since we use 24 steps per env, 10000 steps correspond to 10000/24 = 416.67 learning iterations + # with 24 steps per environment, 10000 steps correspond to about 417 learning iterations gravity = CurrTerm( func=mdp.gravity_range_linear, params={ @@ -610,12 +607,14 @@ class TerminationsCfg: @configclass class FrankaSoftSceneCfg(PresetCfg): - newton_mjwarp_vbd_proxy: _FrankaSoftSceneCfg = _FrankaSoftSceneCfg( + """Scene presets for soft-beam lifting.""" + + newton_mjwarp_vbd_proxy: FrankaSoftBaseSceneCfg = FrankaSoftBaseSceneCfg( num_envs=2048, env_spacing=2.0, replicate_physics=True ) # Isaac Sim PhysX does not support replicating physics for deformable objects - physx: _FrankaSoftSceneCfg = _FrankaSoftSceneCfg(num_envs=2048, env_spacing=2.0, replicate_physics=False) + physx: FrankaSoftBaseSceneCfg = FrankaSoftBaseSceneCfg(num_envs=2048, env_spacing=2.0, replicate_physics=False) isaacsim_physx = physx default = newton_mjwarp_vbd_proxy @@ -625,20 +624,16 @@ class FrankaSoftSceneCfg(PresetCfg): class FrankaSoftCameraSceneCfg(PresetCfg): """Scene presets for visual Franka soft lifting.""" - newton_mjwarp_vbd_proxy: _FrankaSoftCameraSceneCfg = _FrankaSoftCameraSceneCfg( + newton_mjwarp_vbd_proxy: FrankaSoftBaseCameraSceneCfg = FrankaSoftBaseCameraSceneCfg( num_envs=128, env_spacing=2.0, replicate_physics=True ) - physx: _FrankaSoftCameraSceneCfg = _FrankaSoftCameraSceneCfg(num_envs=128, env_spacing=2.0, replicate_physics=False) + physx: FrankaSoftBaseCameraSceneCfg = FrankaSoftBaseCameraSceneCfg( + num_envs=128, env_spacing=2.0, replicate_physics=False + ) isaacsim_physx = physx default = newton_mjwarp_vbd_proxy -@configclass -class _FrankaSoftVisualizerCfg(VisualizerCfg): - window_width: int = 1920 - window_height: int = 1080 - - @configclass class FrankaSoftEnvCfg(ManagerBasedRLEnvCfg): """Manager-based RL environment: Franka Panda lifting a soft beam to a target pose.""" @@ -655,22 +650,17 @@ class FrankaSoftEnvCfg(ManagerBasedRLEnvCfg): events: EventCfg = EventCfg() curriculum: CurriculumCfg = CurriculumCfg() - def __post_init__(self) -> None: + def __post_init__(self): + """Post initialization.""" # general settings self.decimation = 4 self.episode_length_s = 5.0 - # simulation settings - self.sim.dt = 1.0 / 120 + self.sim.dt = 1 / 120 self.sim.render_interval = self.decimation self.sim.physics = PhysicsCfg() - - self.viewer.eye = (0.75, 0.25, 0.65) - self.viewer.lookat = (0.0, 0.75, 0.4) - self.sim.default_visualizer_cfg = _FrankaSoftVisualizerCfg( - eye=self.viewer.eye, - lookat=self.viewer.lookat, - ) + # visualizer settings + self.sim.default_visualizer_cfg = _FrankaSoftVisualizerCfg(eye=(0.75, 0.25, 0.65), lookat=(0.0, 0.75, 0.4)) def play_mode(self): super().play_mode() @@ -685,7 +675,15 @@ class FrankaSoftCameraEnvCfg(FrankaSoftEnvCfg): scene: FrankaSoftCameraSceneCfg = FrankaSoftCameraSceneCfg() observations: FrankaCameraObservationsCfg = FrankaCameraObservationsCfg() - def __post_init__(self) -> None: + def __post_init__(self): super().__post_init__() - # Warm up the RTX render product/annotator (Newton skips the PhysX assets_loading render loop). + # warm up the RTX render product and annotator; Newton skips the PhysX asset-loading render loop self.num_rerenders_on_reset = 2 + + +@configclass +class _FrankaSoftVisualizerCfg(VisualizerCfg): + """Visualizer with a full-HD window for the soft-body environments.""" + + window_width: int = 1920 + window_height: int = 1080 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py index 7abb0fe65e5c..c48ad94563f5 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Camera and observation configurations for the Kuka-Allegro lift environments.""" + from dataclasses import MISSING import isaaclab.sim as sim_utils @@ -13,14 +15,17 @@ from isaaclab.utils import configclass from isaaclab.utils.noise import UniformNoiseCfg as Unoise +import isaaclab_tasks.core.lift.lift_env_cfg as lift +import isaaclab_tasks.core.lift.mdp as mdp from isaaclab_tasks.utils import PresetCfg from isaaclab_tasks.utils.presets import MultiBackendRendererCfg -from ... import lift_env_cfg as lift -from ... import mdp - FINGERTIP_LIST = ["index_link_3", "middle_link_3", "ring_link_3", "thumb_link_3"] +"""Fingertip bodies that carry an object contact sensor.""" +## +# Camera presets +## BASE_CAMERA_CFG = CameraCfg( prim_path="{ENV_REGEX_NS}/Camera", @@ -101,7 +106,7 @@ @configclass class BaseTiledCameraCfg(PresetCfg): - """Tiled camera configurations""" + """Base-mounted camera presets, one per data type and resolution.""" rgb64 = BASE_CAMERA_CFG.replace(data_types=["rgb"], width=64, height=64) rgb128 = BASE_CAMERA_CFG.replace(data_types=["rgb"], width=128, height=128) @@ -145,7 +150,7 @@ class BaseTiledCameraCfg(PresetCfg): @configclass class WristTiledCameraCfg(PresetCfg): - """Tiled camera configurations""" + """Wrist-mounted camera presets, one per data type and resolution.""" rgb64 = WRIST_CAMERA_CFG.replace(data_types=["rgb"], width=64, height=64) rgb128 = WRIST_CAMERA_CFG.replace(data_types=["rgb"], width=128, height=128) @@ -187,14 +192,16 @@ class WristTiledCameraCfg(PresetCfg): default = rgb64 -############################ +## +# MDP settings +## @configclass class StateObservationCfg(lift.ObservationsCfg): - """Kuka Allegro participant scene for Lift Lifting/Reorientation""" + """State observations for the Kuka-Allegro lift tasks.""" - def __post_init__(self: lift.ObservationsCfg): + def __post_init__(self): super().__post_init__() self.proprio.contact = ObsTerm( func=mdp.fingers_contact_force_b, @@ -206,7 +213,7 @@ def __post_init__(self: lift.ObservationsCfg): @configclass class SingleCameraObservationsCfg(StateObservationCfg): - """Observation specifications for the MDP.""" + """State observations plus the base camera image.""" @configclass class BaseImageObsCfg(ObsGroup): @@ -226,10 +233,12 @@ class BaseImageObsCfg(ObsGroup): @configclass class DuoCameraObservationsCfg(SingleCameraObservationsCfg): - """Observation specifications for the MDP.""" + """State observations plus the base and wrist camera images.""" @configclass class WristImageObsCfg(ObsGroup): + """Camera observations for the wrist image group.""" + wrist_observation = ObsTerm( func=mdp.vision_camera, noise=Unoise(n_min=-0.0, n_max=0.0), diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_camera_env_cfg.py index 84a810702481..f44ba89039d8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_camera_env_cfg.py @@ -14,19 +14,18 @@ from isaaclab.sensors import CameraCfg from isaaclab.utils import configclass -from isaaclab_tasks.utils import PresetCfg - -from .camera_cfg import ( +from isaaclab_tasks.core.lift.config.kuka_allegro.camera_cfg import ( BaseTiledCameraCfg, DuoCameraObservationsCfg, SingleCameraObservationsCfg, WristTiledCameraCfg, ) -from .kuka_allegro_env_cfg import ( +from isaaclab_tasks.core.lift.config.kuka_allegro.kuka_allegro_env_cfg import ( KukaAllegroLiftEnvCfg, KukaAllegroReorientEnvCfg, KukaAllegroSceneCfg, ) +from isaaclab_tasks.utils import PresetCfg _SCENE_KWARGS = {"num_envs": 4096, "env_spacing": 3, "replicate_physics": True} @@ -46,13 +45,17 @@ class DuoCameraSceneCfg(KukaAllegroSceneCfg): wrist_camera: CameraCfg = WristTiledCameraCfg() -def _camera_env(base_cls, scene_cls, obs_cls): +def _camera_env( + base_cls: type[KukaAllegroReorientEnvCfg], scene_cls: type[KukaAllegroSceneCfg], obs_cls: type +) -> KukaAllegroReorientEnvCfg: """Build a camera env config by swapping a camera scene and image observations onto a state env.""" return base_cls(scene=scene_cls(**_SCENE_KWARGS), observations=obs_cls()) @configclass class KukaAllegroReorientCameraEnvCfg(PresetCfg): + """Camera variants of the Kuka-Allegro reorientation environment.""" + single_camera = _camera_env(KukaAllegroReorientEnvCfg, SingleCameraSceneCfg, SingleCameraObservationsCfg) duo_camera = _camera_env(KukaAllegroReorientEnvCfg, DuoCameraSceneCfg, DuoCameraObservationsCfg) default = single_camera @@ -60,6 +63,8 @@ class KukaAllegroReorientCameraEnvCfg(PresetCfg): @configclass class KukaAllegroLiftCameraEnvCfg(PresetCfg): + """Camera variants of the Kuka-Allegro lifting environment.""" + single_camera = _camera_env(KukaAllegroLiftEnvCfg, SingleCameraSceneCfg, SingleCameraObservationsCfg) duo_camera = _camera_env(KukaAllegroLiftEnvCfg, DuoCameraSceneCfg, DuoCameraObservationsCfg) default = single_camera diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_env_cfg.py index 701b99ebdf90..f8bee6c03478 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the Kuka-Allegro lift and reorient environments.""" + from isaaclab.assets import ArticulationCfg from isaaclab.managers import EventTermCfg as EventTerm from isaaclab.managers import RewardTermCfg as RewTerm @@ -10,23 +12,30 @@ from isaaclab.sensors import CameraCfg, ContactSensorCfg from isaaclab.utils import configclass -from isaaclab_assets.robots import KUKA_ALLEGRO_CFG +import isaaclab_tasks.core.lift.lift_env_cfg as lift +import isaaclab_tasks.core.lift.mdp as mdp +from isaaclab_tasks.core.lift.config.kuka_allegro.camera_cfg import FINGERTIP_LIST, StateObservationCfg -from ... import lift_env_cfg as lift -from ... import mdp -from .camera_cfg import StateObservationCfg +from isaaclab_assets.robots import KUKA_ALLEGRO_CFG -FINGERTIP_LIST = ["index_link_3", "middle_link_3", "ring_link_3", "thumb_link_3"] THUMB_SENSOR = "thumb_link_3_object_s" +"""Contact sensor of the thumb.""" + FINGER_SENSORS = [f"{name}_object_s" for name in FINGERTIP_LIST if name != "thumb_link_3"] +"""Contact sensors of the remaining fingers.""" + + +## +# Scene definition +## @configclass class KukaAllegroSceneCfg(lift.SceneCfg): - """KukaAllegro scene for the Lift and Reorient tasks. + """Kuka-Allegro scene for the lift and reorient tasks. - The ``base_camera`` / ``wrist_camera`` slots are left unset (``None``) for the state task; the - camera env config populates them (see ``kuka_allegro_camera_env_cfg``). + The ``base_camera`` and ``wrist_camera`` slots are left unset for the state task; the camera + environment configuration populates them. """ robot: ArticulationCfg = KUKA_ALLEGRO_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") @@ -46,13 +55,22 @@ def __post_init__(self): ) +## +# MDP settings +## + + @configclass class KukaAllegroRelJointPosActionCfg: + """Relative joint position targets for all joints.""" + action = mdp.RelativeJointPositionActionCfg(asset_name="robot", joint_names=[".*"], scale=0.1) @configclass class KukaAllegroReorientRewardCfg(lift.RewardsCfg): + """Reward terms for the MDP, with the Allegro finger contact sensors filled in.""" + good_finger_contact = RewTerm( func=mdp.contacts, weight=1.0, @@ -79,14 +97,21 @@ def __post_init__(self): self.success.params["finger_names"] = FINGER_SENSORS +## +# Environment configuration +## + + @configclass class KukaAllegroMixinCfg: + """Kuka-Allegro specific scene, observation, action and reward terms, mixed into the task configurations.""" + scene: KukaAllegroSceneCfg = KukaAllegroSceneCfg(num_envs=4096, env_spacing=3, replicate_physics=True) rewards: KukaAllegroReorientRewardCfg = KukaAllegroReorientRewardCfg() observations: StateObservationCfg = StateObservationCfg() actions: KukaAllegroRelJointPosActionCfg = KukaAllegroRelJointPosActionCfg() - def __post_init__(self: lift.ReorientEnvCfg): + def __post_init__(self): super().__post_init__() self.commands.object_pose.body_name = "palm_link" events = self.events.conditional_reset.params["terms"] @@ -106,7 +131,7 @@ def __post_init__(self: lift.ReorientEnvCfg): diversity_feature = self.events.conditional_reset.params.get("diversity_feature") if diversity_feature is not None: diversity_feature.body_names = ["palm_link", ".*_tip"] - # finger closing-speed DR: armature sets tau/M. + # finger closing-speed randomization: the armature sets the effort-to-inertia ratio self.events.finger_closing_speed = EventTerm( func=mdp.randomize_joint_parameters, mode="startup", @@ -121,9 +146,9 @@ def __post_init__(self: lift.ReorientEnvCfg): @configclass class KukaAllegroReorientEnvCfg(KukaAllegroMixinCfg, lift.ReorientEnvCfg): - pass + """Kuka-Allegro object reorientation environment.""" @configclass class KukaAllegroLiftEnvCfg(KukaAllegroMixinCfg, lift.LiftEnvCfg): - pass + """Kuka-Allegro object lifting environment.""" 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 a2807f5c29fe..71f9b6f518c7 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 @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Base configuration for the lift and reorient (arm plus hand) environments.""" + from dataclasses import MISSING from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg, NewtonCollisionPipelineCfg, NewtonShapeCfg @@ -28,28 +30,34 @@ from isaaclab.utils.noise import UniformNoiseCfg as Unoise from isaaclab.visualizers import VisualizerCfg +import isaaclab_tasks.core.lift.mdp as mdp +from isaaclab_tasks.core.lift.adr_curriculum import CurriculumCfg from isaaclab_tasks.utils import PresetCfg -from . import mdp -from .adr_curriculum import CurriculumCfg +## +# Scene assets +## TABLE_SPAWN_CFG = sim_utils.CuboidCfg( size=(0.8, 1.5, 0.04), rigid_props=sim_utils.UsdPhysicsRigidBodyCfg(kinematic_enabled=True), collision_props=sim_utils.UsdPhysicsCollisionCfg(), - # trick: we let visualizer's color to show the table with success coloring + # spawned invisible: the command term's success markers draw the table, tinted by success visible=False, ) - +"""Table the object rests on.""" OBJECT_PHYSICS = { "physics_material": RigidBodyMaterialCfg(static_friction=0.5), "collision_props": [PhysxCollisionCfg(contact_offset=0.002)], } +"""Physics properties shared by the graspable object shapes.""" @configclass class ObjectCfg(PresetCfg): + """Graspable object presets: a set of primitive shapes, or a single cube for OvPhysX.""" + shapes = sim_utils.MultiAssetSpawnerCfg( assets_cfg=[ MeshCuboidCfg(size=(0.05, 0.1, 0.1), **OBJECT_PHYSICS), @@ -91,9 +99,14 @@ class ObjectCfg(PresetCfg): ovphysx = cube +## +# Scene definition +## + + @configclass class SceneCfg(InteractiveSceneCfg): - """Lift Scene for multi-objects Lifting""" + """Scene with a robot, a table and the graspable object.""" # robot robot: ArticulationCfg = MISSING @@ -130,6 +143,11 @@ class SceneCfg(InteractiveSceneCfg): ) +## +# MDP settings +## + + @configclass class CommandsCfg: """Command terms for the MDP.""" @@ -188,13 +206,11 @@ class ProprioObsCfg(ObsGroup): hand_tips_state_b = ObsTerm( func=mdp.body_state_b, noise=Unoise(n_min=-0.0, n_max=0.0), - # good behaving number for position in m, velocity in m/s, rad/s, - # and quaternion are unlikely to exceed -2 to 2 range + # positions [m] and quaternions stay well within this range clip=(-2.0, 2.0), params={ - # pose-only: body velocities are the most engine-sensitive observables - # (derivative signals amplify solver differences) and do not transfer - # across physics backends; the observation history carries velocity info + # pose-only: body velocities are the most solver-sensitive observables and do not transfer + # across physics backends; the observation history carries the velocity information "include_vel": False, "body_asset_cfg": SceneEntityCfg("robot"), "base_asset_cfg": SceneEntityCfg("robot"), @@ -214,7 +230,7 @@ class PerceptionObsCfg(ObsGroup): object_point_cloud = ObsTerm( func=mdp.object_point_cloud_b, noise=Unoise(n_min=-0.0, n_max=0.0), - clip=(-2.0, 2.0), # clamp between -2 m to 2 m + clip=(-2.0, 2.0), # [m] params={"num_points": 64, "flatten": True}, ) @@ -233,7 +249,7 @@ def __post_init__(self): @configclass class EventCfg: - """Reset-mode events (shared by all physics backends).""" + """Configuration for events.""" robot_physics_material = EventTerm( func=mdp.randomize_rigid_body_material, @@ -301,9 +317,8 @@ class EventCfg: }, ) - # Gravity scheduling is a deliberate curriculum trick — starting with no - # gravity (easy) and gradually introducing full gravity (hard) makes learning - # smoother and removes the need for a separate "Lift" reward. + # gravity curriculum: starting without gravity and gradually introducing full gravity makes + # learning smoother and removes the need for a separate lifting reward variable_gravity = EventTerm( func=mdp.randomize_physics_scene_gravity, mode="reset", @@ -361,10 +376,10 @@ class EventCfg: "asset_cfg": SceneEntityCfg("object"), }, ), - # spawn-in-hand curriculum: a small share of episodes starts with the - # object at the gripper (uniform random orientation, small body-frame - # offset); interpenetrating draws are rejected by object_robot_clearance. - # Must stay LAST: it reads the gripper pose after the robot reset terms. + # spawn-in-hand curriculum: a small share of episodes starts with the object at the + # gripper (uniform random orientation, small body-frame offset); interpenetrating draws + # are rejected by object_robot_clearance. Must stay last: it reads the gripper pose after + # the robot reset terms. "reset_object_to_target": EventTerm( func="isaaclab_tasks.core.lift.mdp.events:reset_to_target", mode="reset", @@ -415,7 +430,7 @@ class EventCfg: @configclass class ActionsCfg: - pass + """Action specifications for the MDP, set by the robot-specific configurations.""" @configclass @@ -462,7 +477,7 @@ class RewardsCfg: }, ) - early_termination = RewTerm(func=mdp.is_terminated_term, weight=-50, params={"term_keys": ["abnormal_robot"]}) + early_termination = RewTerm(func=mdp.is_terminated_term, weight=-50.0, params={"term_keys": ["abnormal_robot"]}) @configclass @@ -482,8 +497,15 @@ class TerminationsCfg: abnormal_robot = DoneTerm(func=mdp.joint_vel_out_of_limit) +## +# Physics backend presets +## + + @configclass class PhysicsCfg(PresetCfg): + """Physics backend presets for the lift environments.""" + isaacsim_physx = PhysxCfg( bounce_threshold_velocity=0.01, gpu_max_rigid_patch_count=4 * 5 * 2**15, @@ -516,9 +538,14 @@ class PhysicsCfg(PresetCfg): default = newton_mjwarp +## +# Environment configuration +## + + @configclass class ReorientEnvCfg(ManagerBasedRLEnvCfg): - """Lift reorientation task definition, also the base definition for derivative Lift task and evaluation task""" + """Object reorientation environment, also the base of the lift environment.""" # Scene settings scene: SceneCfg = SceneCfg(num_envs=4096, env_spacing=3, replicate_physics=True) @@ -534,7 +561,6 @@ class ReorientEnvCfg(ManagerBasedRLEnvCfg): def validate_config(self): """Check for invalid preset combinations after resolution.""" - warp_supported = { "rgb", "depth", @@ -562,22 +588,19 @@ def __post_init__(self): """Post initialization.""" # general settings self.decimation = 4 # 30 Hz - - # *single-goal setup - self.commands.object_pose.position_only = False self.episode_length_s = 12.0 self.is_finite_horizon = False - + # commands: track the full pose + self.commands.object_pose.position_only = False # simulation settings self.sim.dt = 1 / 120 self.sim.render_interval = self.decimation self.sim.physics = PhysicsCfg() + # visualizer settings self.sim.default_visualizer_cfg = VisualizerCfg(eye=(-2.25, 0.0, 0.75), lookat=(0.0, 0.0, 0.45)) def play_mode(self): - # play-mode overrides of parent super().play_mode() - self.commands.object_pose.debug_vis = True # the bank shapes what a policy trains on; at play it only has to supply starts for the # handful of environments the parent left, so it is harvested small and taken as it comes @@ -593,18 +616,18 @@ def play_mode(self): self.curriculum.disable_observation_noise_terms() +@configclass class LiftEnvCfg(ReorientEnvCfg): - """Lift task definition.""" + """Object lifting environment: the reorientation environment tracking position only.""" def __post_init__(self): super().__post_init__() - self.rewards.orientation_tracking = None # no orientation reward + # commands and rewards: track the position only self.commands.object_pose.position_only = True + self.rewards.orientation_tracking = None if self.curriculum is not None: - self.rewards.success.params["rot_std"] = None # make success reward not consider orientation + self.rewards.success.params["rot_std"] = None def play_mode(self): - # play-mode overrides of parent super().play_mode() - self.commands.object_pose.position_only = True diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/__init__.py index e14e0f6d52c5..72a61d88a5ce 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/__init__.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""MDP terms specific to the lift and reorient (arm plus hand) environments.""" + from isaaclab.utils.module import lazy_export lazy_export() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/__init__.pyi index de29a7e9c6b9..757dabf55653 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/__init__.pyi @@ -4,68 +4,65 @@ # SPDX-License-Identifier: BSD-3-Clause __all__ = [ + "CableSegmentGoalDistance", + "CableUniformPoseCommandCfg", + "DeformableComGoalDistance", + "DeformableSampledPointsInRobotRootFrame", + "DeformableUniformPoseCommandCfg", "GraspTravelDistanceCfg", "MeshClearanceCfg", + "ObjectUniformPoseCommandCfg", "SlabClearanceCfg", "SuccessMonitor", "SuccessMonitorCfg", - "grasp_travel_distance", - "mesh_clearance", - "slab_clearance", - "conditional_reset", - "reset_joints_shared_offset", - "reset_to_target", - "get_reset_state", - "set_reset_state", - "joint_vel_out_of_sim_limit", - "cable_outside_bounds", - "deformable_outside_bounds", - "deformable_lifting", - "deformable_com_goal_reached", - "deformable_com_ee_distance", - "reset_cable_state_uniform", - "DeformableComGoalDistance", - "reset_deformable_over_support", - "gravity_range_linear", - "DeformableUniformPoseCommandCfg", + "abnormal_robot_state", + "body_state_b", + "cable_ee_distance", "cable_lifting", + "cable_outside_bounds", "cable_segment_goal_reached", - "cable_ee_distance", - "CableSegmentGoalDistance", - "ObjectUniformPoseCommandCfg", - "DifficultyScheduler", - "initial_final_interpolate_fn", - "DeformableSampledPointsInRobotRootFrame", - "body_state_b", - "deformable_com_in_robot_root_frame", - "fingers_contact_force_b", - "object_point_cloud_b", - "CableUniformPoseCommandCfg", - "object_quat_b", - "vision_camera", - "contacts", + "cable_segment_positions_in_robot_root_frame", + "conditional_reset", "contact_count", + "contacts", + "deformable_com_ee_distance", + "deformable_com_goal_reached", + "deformable_com_in_robot_root_frame", "deformable_ee_distance", - "cable_segment_positions_in_robot_root_frame", + "deformable_lifting", + "deformable_outside_bounds", + "ee_below_minimum", + "fingers_contact_force_b", + "get_reset_state", + "grasp_travel_distance", + "gravity_range_linear", "gripper_close_action", + "joint_vel_out_of_sim_limit", + "mesh_clearance", "object_ee_distance", + "object_point_cloud_b", + "object_quat_b", + "object_reached_goal", "orientation_command_error_tanh", "orientation_command_progress", + "out_of_bound", "position_command_error_tanh", "position_command_progress", + "reset_cable_state_uniform", + "reset_deformable_over_support", + "reset_joints_shared_offset", + "reset_to_target", + "set_reset_state", + "slab_clearance", "success_reward", - "abnormal_robot_state", - "ee_below_minimum", - "object_reached_goal", - "out_of_bound", + "vision_camera", ] -from isaaclab.envs.mdp import * +from isaaclab_tasks.utils.success_monitor import SuccessMonitor, SuccessMonitorCfg from .commands import CableUniformPoseCommandCfg, DeformableUniformPoseCommandCfg, ObjectUniformPoseCommandCfg -from .curriculums import DifficultyScheduler, gravity_range_linear, initial_final_interpolate_fn +from .curriculums import gravity_range_linear from .events import ( - SuccessMonitor, conditional_reset, grasp_travel_distance, mesh_clearance, @@ -75,7 +72,7 @@ from .events import ( reset_to_target, slab_clearance, ) -from .events_cfg import GraspTravelDistanceCfg, MeshClearanceCfg, SlabClearanceCfg, SuccessMonitorCfg +from .events_cfg import GraspTravelDistanceCfg, MeshClearanceCfg, SlabClearanceCfg from .observations import ( DeformableSampledPointsInRobotRootFrame, body_state_b, @@ -90,8 +87,8 @@ from .rewards import ( CableSegmentGoalDistance, DeformableComGoalDistance, cable_ee_distance, - cable_segment_goal_reached, cable_lifting, + cable_segment_goal_reached, contact_count, contacts, deformable_com_ee_distance, @@ -116,3 +113,5 @@ from .terminations import ( out_of_bound, ) from .utils import get_reset_state, set_reset_state + +from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/__init__.py index e14e0f6d52c5..cb32f1fd6d05 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/__init__.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Command terms specific to the lift environments.""" + from isaaclab.utils.module import lazy_export lazy_export() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/__init__.pyi index 9f8253675eec..e6e7c4614a10 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/__init__.pyi @@ -3,10 +3,6 @@ # # SPDX-License-Identifier: BSD-3-Clause -__all__ = [ - "DeformableUniformPoseCommandCfg", - "ObjectUniformPoseCommandCfg", - "CableUniformPoseCommandCfg", -] +__all__ = ["CableUniformPoseCommandCfg", "DeformableUniformPoseCommandCfg", "ObjectUniformPoseCommandCfg"] from .pose_commands_cfg import CableUniformPoseCommandCfg, DeformableUniformPoseCommandCfg, ObjectUniformPoseCommandCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/pose_commands.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/pose_commands.py index 99ad2a712bf5..9e3121393c12 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/pose_commands.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/pose_commands.py @@ -3,8 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause - -"""Sub-module containing command generators for pose tracking.""" +"""Object pose commands for the lift environments.""" from __future__ import annotations @@ -15,6 +14,7 @@ from isaaclab.assets import Asset, AssetBase from isaaclab.managers import CommandTerm +from isaaclab.markers import VisualizationMarkers from isaaclab.utils.leapp import POSE7_ELEMENT_NAMES from isaaclab.utils.math import combine_frame_transforms, compute_pose_error, quat_from_euler_xyz, quat_unique @@ -30,28 +30,16 @@ class ObjectUniformPoseCommand(CommandTerm): - """Uniform pose command generator for an object (in the robot base frame). - - This command term samples target object poses by: - • Drawing (x, y, z) uniformly within configured Cartesian bounds, and - • Drawing roll-pitch-yaw uniformly within configured ranges, then converting - to a quaternion (x, y, z, w). Optionally makes quaternions unique by enforcing - a positive real part. - - Frames: - Targets are defined in the robot's *base frame*. For metrics/visualization, - targets are transformed into the *world frame* using the robot root pose. - - Outputs: - The command buffer has shape (num_envs, 7): ``(x, y, z, qx, qy, qz, qw)``. + """Uniform pose command generator for an object, expressed in the robot root frame. - Metrics: - `position_error` and `orientation_error` are computed between the commanded - world-frame pose and the object's current world-frame pose. + The position is drawn uniformly within the configured Cartesian bounds and the orientation from + uniformly drawn roll, pitch and yaw angles, converted to an ``(x, y, z, w)`` quaternion that is + optionally made unique by enforcing a positive real part. The command buffer has shape + ``(num_envs, 7)`` as ``(x, y, z, qx, qy, qz, qw)``. - Config: - `cfg` must provide the sampling ranges, whether to enforce quaternion uniqueness, - and optional visualization settings. + For the metrics and visualization the command is transformed into the world frame with the robot + root pose. The ``position_error`` and ``orientation_error`` metrics compare the commanded and the + object's current world-frame poses. """ cfg: ObjectUniformPoseCommandCfg @@ -67,14 +55,12 @@ def __init__(self, cfg: ObjectUniformPoseCommandCfg, env: ManagerBasedEnv): # initialize the base class super().__init__(cfg, env) - # extract the robot and body index for which the command is generated + # extract the robot and the object for which the command is generated self.robot: Articulation = env.scene[cfg.asset_name] self.object: RigidObject = env.scene[cfg.object_name] - self.success_vis_asset: RigidObject | Asset | None + self.success_vis_asset: RigidObject | Asset | None = None if cfg.success_vis_asset_name in env.scene.keys(): self.success_vis_asset = env.scene[cfg.success_vis_asset_name] - else: - self.success_vis_asset = None if isinstance(self.success_vis_asset, Asset) and not isinstance(self.success_vis_asset, AssetBase): offset = torch.tensor(self.success_vis_asset.cfg.init_state.pos, device=self.device) self._static_success_vis_pos_w = env.scene.env_origins + offset @@ -90,8 +76,7 @@ def __init__(self, cfg: ObjectUniformPoseCommandCfg, env: ManagerBasedEnv): self.metrics["position_error"] = torch.zeros(self.num_envs, device=self.device) if not self.cfg.position_only: self.metrics["orientation_error"] = torch.zeros(self.num_envs, device=self.device) - from isaaclab.markers import VisualizationMarkers - + # -- success markers, always visible self.success_visualizer = VisualizationMarkers(self.cfg.success_visualizer_cfg) self.success_visualizer.set_visibility(True) if self.success_vis_asset is not None: @@ -184,8 +169,6 @@ def _update_command(self): def _set_debug_vis_impl(self, debug_vis: bool): if debug_vis: if not hasattr(self, "goal_visualizer"): - from isaaclab.markers import VisualizationMarkers - self.goal_visualizer = VisualizationMarkers(self.cfg.goal_pose_visualizer_cfg) self.curr_visualizer = VisualizationMarkers(self.cfg.curr_pose_visualizer_cfg) # set their visibility to true diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/pose_commands_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/pose_commands_cfg.py index 63180a1e9ea6..118228490b9c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/pose_commands_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/pose_commands_cfg.py @@ -3,6 +3,10 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the object pose commands of the lift environments.""" + +from __future__ import annotations + from dataclasses import MISSING from typing import TYPE_CHECKING @@ -37,7 +41,7 @@ class ObjectUniformPoseCommandCfg(CommandTermCfg): """Configuration for uniform pose command generator.""" - class_type: type["ObjectUniformPoseCommand"] | str = "{DIR}.pose_commands:ObjectUniformPoseCommand" + class_type: type[ObjectUniformPoseCommand] | str = "{DIR}.pose_commands:ObjectUniformPoseCommand" asset_name: str = MISSING """Name of the coordinate referencing asset in the environment for which the commands are generated respect to.""" @@ -100,14 +104,14 @@ class Ranges: class DeformableUniformPoseCommandCfg(ObjectUniformPoseCommandCfg): """Configuration for the deformable uniform pose command generator.""" - class_type: type["DeformableUniformPoseCommand"] | str = "{DIR}.pose_commands:DeformableUniformPoseCommand" + class_type: type[DeformableUniformPoseCommand] | str = "{DIR}.pose_commands:DeformableUniformPoseCommand" @configclass class CableUniformPoseCommandCfg(ObjectUniformPoseCommandCfg): """Configuration for a cable segment uniform pose command generator.""" - class_type: type["CableUniformPoseCommand"] | str = "{DIR}.pose_commands:CableUniformPoseCommand" + class_type: type[CableUniformPoseCommand] | str = "{DIR}.pose_commands:CableUniformPoseCommand" segment_index: int = MISSING """Zero-based cable segment index tracked by the command.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/curriculums.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/curriculums.py index 8a3884fad20c..4dfad1e07dd7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/curriculums.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/curriculums.py @@ -3,88 +3,17 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Curriculum terms for the lift environments.""" + from __future__ import annotations from collections.abc import Sequence from typing import TYPE_CHECKING -import torch - -from isaaclab.envs import mdp -from isaaclab.managers import ManagerTermBase - if TYPE_CHECKING: from isaaclab.envs import ManagerBasedRLEnv -def initial_final_interpolate_fn(env: ManagerBasedRLEnv, env_id, data, initial_value, final_value, difficulty_term_str): - """Interpolate between initial and final values scaled by the current difficulty fraction. - - Works on arbitrarily nested structures of lists/tuples; scalars (int/float) - are interpolated at the leaves. - """ - difficulty_term: DifficultyScheduler = getattr(env.curriculum_manager.cfg, difficulty_term_str).func - frac = difficulty_term.difficulty_frac - if frac < 0.1: - return mdp.modify_env_param.NO_CHANGE - # Python scalars: interpolate host-side, a device round-trip syncs the stream - return _recurse(initial_value, final_value, data, frac) - - -def _recurse(iv_elem, fv_elem, data_elem, frac): - if isinstance(data_elem, Sequence) and not isinstance(data_elem, (str, bytes)): - return type(data_elem)(_recurse(iv_e, fv_e, d_e, frac) for iv_e, fv_e, d_e in zip(iv_elem, fv_elem, data_elem)) - new_val = frac * (fv_elem - iv_elem) + iv_elem - if isinstance(data_elem, int): - return int(new_val) - return new_val - - -class DifficultyScheduler(ManagerTermBase): - """Adaptive difficulty scheduler for curriculum learning. - - Promotes difficulty when the ``success`` reward term's sticky - :attr:`succeeded` flag is ``True`` for an environment at episode end, - meaning the agent achieved the success condition at least once during the - episode. Demotes (unless ``promotion_only`` is set) otherwise. - - The normalized average difficulty across environments is exposed as - :attr:`difficulty_frac` for use in curriculum interpolation. - """ - - def __init__(self, cfg, env): - super().__init__(cfg, env) - init_difficulty = self.cfg.params.get("init_difficulty", 0) - self.current_adr_difficulties = torch.ones(env.num_envs, device=env.device) * init_difficulty - self.difficulty_frac = 0 - - def get_state(self): - return self.current_adr_difficulties - - def set_state(self, state: torch.Tensor): - self.current_adr_difficulties = state.clone().to(self._env.device) - - def __call__( - self, - env: ManagerBasedRLEnv, - env_ids: Sequence[int], - init_difficulty: int = 0, - min_difficulty: int = 0, - max_difficulty: int = 50, - promotion_only: bool = False, - ): - succeeded = env.reward_manager.get_term_cfg("success").func.succeeded[env_ids] - demot = self.current_adr_difficulties[env_ids] if promotion_only else self.current_adr_difficulties[env_ids] - 1 - self.current_adr_difficulties[env_ids] = torch.where( - succeeded, - self.current_adr_difficulties[env_ids] + 1, - demot, - ).clamp(min=min_difficulty, max=max_difficulty) - # Python float: downstream ADR terms compare and interpolate host-side - self.difficulty_frac = (torch.mean(self.current_adr_difficulties) / max(max_difficulty, 1)).item() - return self.difficulty_frac - - def gravity_range_linear( env: ManagerBasedRLEnv, _env_ids: Sequence[int], diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/events.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/events.py index 021dc9ce8e85..ff42d5a95602 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/events.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Event terms for the lift tasks.""" +"""Event terms for the lift environments.""" from __future__ import annotations @@ -580,7 +580,7 @@ class mesh_clearance(ManagerTermBase): Reset draws can place the object overlapping the arm; the solver resolves the overlap ballistically at episode birth. Checks both the object's surface point cloud against the robot's collision meshes and the robot's collision vertices against the object's collision - mesh with Warp signed-distance queries — the winding-number sign catches full containment. + mesh with Warp signed-distance queries; the winding-number sign catches full containment. The object point cloud comes from the same sampler as the point-cloud observation (per clone-plan prototype, geometry-keyed cache), so with the default count the cloud is diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/events_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/events_cfg.py index 1584bc7b5fb7..7e5a41342e4e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/events_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/events_cfg.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Configuration for the lift event terms.""" +"""Configuration for the event terms of the lift environments.""" from __future__ import annotations @@ -13,8 +13,6 @@ from isaaclab.managers import ManagerTermBaseCfg from isaaclab.utils import configclass -from isaaclab_tasks.utils.success_monitor import SuccessMonitorCfg as SuccessMonitorCfg - @configclass class MeshClearanceCfg(ManagerTermBaseCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/observations.py index 5409ede936a1..2fcf26d0b8dc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/observations.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Observation terms for the lift environments.""" + from __future__ import annotations from collections.abc import Sequence @@ -10,13 +12,16 @@ import torch -from isaaclab.managers import ManagerTermBase, SceneEntityCfg +from isaaclab.managers import ManagerTermBase, ObservationTermCfg, SceneEntityCfg +from isaaclab.markers import VisualizationMarkers +from isaaclab.markers.config import RAY_CASTER_MARKER_CFG from isaaclab.utils.math import quat_apply, quat_apply_inverse, quat_inv, quat_mul, subtract_frame_transforms +from .utils import sample_object_point_cloud + if TYPE_CHECKING: from isaaclab.assets import Articulation, CableObject, DeformableObject, RigidObject from isaaclab.envs import ManagerBasedRLEnv - from isaaclab.managers import ObservationTermCfg from isaaclab.sensors import Camera @@ -33,7 +38,7 @@ def object_quat_b( object_cfg: Scene entity for the object. Defaults to ``SceneEntityCfg("object")``. Returns: - Tensor of shape ``(num_envs, 4)``: object quaternion ``(w, x, y, z)`` in the robot root frame. + Tensor of shape ``(num_envs, 4)``: object quaternion ``(x, y, z, w)`` in the robot root frame. """ robot: RigidObject = env.scene[robot_cfg.name] object: RigidObject = env.scene[object_cfg.name] @@ -44,7 +49,7 @@ class body_state_b(ManagerTermBase): """Body state (pos, quat, lin vel, ang vel) in the base asset's root frame. The state for each body is stacked horizontally as - ``[position(3), quaternion(4)(wxyz), linvel(3), angvel(3)]`` and then concatenated over bodies. + ``[position(3), quaternion(4)(xyzw), linvel(3), angvel(3)]`` and then concatenated over bodies. The body indices are baked to a device tensor at construction. """ @@ -77,20 +82,19 @@ def __call__( """ body_asset: Articulation = env.scene[body_asset_cfg.name] base_asset: Articulation = env.scene[base_asset_cfg.name] - # get world pose of bodies - body_pos_w = body_asset.data.body_pos_w.torch[:, self._body_ids].view(-1, 3) - body_quat_w = body_asset.data.body_quat_w.torch[:, self._body_ids].view(-1, 4) - num_bodies = int(body_pos_w.shape[0] / env.num_envs) - # get world pose of base frame - root_pos_w = base_asset.data.root_link_pos_w.torch.unsqueeze(1).repeat_interleave(num_bodies, dim=1).view(-1, 3) - root_quat_w = ( - base_asset.data.root_link_quat_w.torch.unsqueeze(1).repeat_interleave(num_bodies, dim=1).view(-1, 4) - ) + # world pose of the bodies, flattened over environments + body_pos_w = body_asset.data.body_pos_w.torch[:, self._body_ids] + num_bodies = body_pos_w.shape[1] + body_pos_w = body_pos_w.reshape(-1, 3) + body_quat_w = body_asset.data.body_quat_w.torch[:, self._body_ids].reshape(-1, 4) + # world pose of the base frame, broadcast over the bodies + root_pos_w = base_asset.data.root_link_pos_w.torch.unsqueeze(1).expand(-1, num_bodies, -1).reshape(-1, 3) + root_quat_w = base_asset.data.root_link_quat_w.torch.unsqueeze(1).expand(-1, num_bodies, -1).reshape(-1, 4) # transform from world body pose to local body pose body_pos_b, body_quat_b = subtract_frame_transforms(root_pos_w, root_quat_w, body_pos_w, body_quat_w) - # note: body velocities are the most engine/solver-sensitive observables (derivative - # signals amplify integrator and contact-response differences); pose-only states with - # observation history transfer across physics backends, velocity states do not. + # body velocities are the most solver-sensitive observables (derivative signals amplify integrator + # and contact-response differences); pose-only states with observation history transfer across + # physics backends, velocity states do not if include_vel: body_lin_vel_w = body_asset.data.body_lin_vel_w.torch[:, self._body_ids].view(-1, 3) body_ang_vel_w = body_asset.data.body_ang_vel_w.torch[:, self._body_ids].view(-1, 3) @@ -105,43 +109,25 @@ def __call__( class object_point_cloud_b(ManagerTermBase): """Object surface point cloud expressed in a reference asset's root frame. - Points are pre-sampled on the object's surface in its local frame and transformed to world, - then into the reference (e.g., robot) root frame. Optionally visualizes the points. - - Args (from ``cfg.params``): - object_cfg: Scene entity for the object to sample. Defaults to ``SceneEntityCfg("object")``. - ref_asset_cfg: Scene entity providing the reference frame. Defaults to ``SceneEntityCfg("robot")``. - num_points: Number of points to sample on the object surface. Defaults to ``10``. - visualize: Whether to draw markers for the points. Defaults to ``True``. - static: If ``True``, cache world-space points on reset and reuse them (no per-step resampling). - - Returns (from ``__call__``): - If ``flatten=False``: tensor of shape ``(num_envs, num_points, 3)``. - If ``flatten=True``: tensor of shape ``(num_envs, 3 * num_points)``. + Points are pre-sampled on the object's surface in its local frame at construction, transformed to + the world frame every step, and then into the reference (e.g. robot) root frame. The points can + optionally be drawn as markers. """ - def __init__(self, cfg, env: ManagerBasedRLEnv): + def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): super().__init__(cfg, env) - - self.object_cfg: SceneEntityCfg = cfg.params.get("object_cfg", SceneEntityCfg("object")) - self.ref_asset_cfg: SceneEntityCfg = cfg.params.get("ref_asset_cfg", SceneEntityCfg("robot")) + object_cfg: SceneEntityCfg = cfg.params.get("object_cfg", SceneEntityCfg("object")) + ref_asset_cfg: SceneEntityCfg = cfg.params.get("ref_asset_cfg", SceneEntityCfg("robot")) num_points: int = cfg.params.get("num_points", 10) - self.object: RigidObject = env.scene[self.object_cfg.name] - self.ref_asset: Articulation = env.scene[self.ref_asset_cfg.name] - # lazy initialize visualizer and point cloud - if cfg.params.get("visualize", True): - from isaaclab.markers import VisualizationMarkers - from isaaclab.markers.config import RAY_CASTER_MARKER_CFG - - ray_cfg = RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/ObservationPointCloud") - ray_cfg.markers["hit"].radius = 0.0025 - self.visualizer = VisualizationMarkers(ray_cfg) - from .utils import sample_object_point_cloud - - self.points_local = sample_object_point_cloud( - env.num_envs, num_points, self.object.cfg.prim_path, device=env.device - ) + self.object: RigidObject = env.scene[object_cfg.name] + self.ref_asset: Articulation = env.scene[ref_asset_cfg.name] + self.points_local = sample_object_point_cloud(env.num_envs, num_points, self.object.cfg.prim_path, env.device) self.points_w = torch.zeros_like(self.points_local) + if cfg.params.get("visualize", True): + marker_cfg = RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/ObservationPointCloud") + marker_cfg.markers["hit"].radius = 0.0025 + self.visualizer = VisualizationMarkers(marker_cfg) + self._marker_env_ids = torch.arange(env.num_envs, device=env.device).repeat_interleave(num_points) def __call__( self, @@ -151,42 +137,30 @@ def __call__( num_points: int = 10, flatten: bool = False, visualize: bool = True, - ): + ) -> torch.Tensor: """Compute the object point cloud in the reference asset's root frame. - Note: - Points are pre-sampled at initialization using ``self.num_points``; the ``num_points`` argument is - kept for API symmetry and does not change the sampled set at runtime. - Args: env: The environment. - ref_asset_cfg: Reference frame provider (root). Defaults to ``SceneEntityCfg("robot")``. - object_cfg: Object to sample. Defaults to ``SceneEntityCfg("object")``. - num_points: Unused at runtime; see note above. - flatten: If ``True``, return a flattened tensor ``(num_envs, 3 * num_points)``. - visualize: If ``True``, draw markers for the points. + ref_asset_cfg: Scene entity providing the reference (root) frame. Defaults to ``SceneEntityCfg("robot")``. + object_cfg: Scene entity of the object to sample. Defaults to ``SceneEntityCfg("object")``. + num_points: Number of surface points. Read at construction; changing it at runtime has no effect. + flatten: Whether to return the points as ``(num_envs, 3 * num_points)`` instead of + ``(num_envs, num_points, 3)``. + visualize: Whether to draw markers for the points. Read at construction and per call. Returns: - Tensor of shape ``(num_envs, num_points, 3)`` or flattened if requested. + Object surface points [m] in the reference root frame, flattened if requested. """ - ref_pos_w = self.ref_asset.data.root_pos_w.torch.unsqueeze(1).repeat(1, num_points, 1) - ref_quat_w = self.ref_asset.data.root_quat_w.torch.unsqueeze(1).repeat(1, num_points, 1) - - object_pos_w = self.object.data.root_pos_w.torch.unsqueeze(1).repeat(1, num_points, 1) - object_quat_w = self.object.data.root_quat_w.torch.unsqueeze(1).repeat(1, num_points, 1) - # apply rotation + translation + object_pos_w = self.object.data.root_pos_w.torch.unsqueeze(1) + object_quat_w = self.object.data.root_quat_w.torch.unsqueeze(1).expand(-1, num_points, -1) self.points_w = quat_apply(object_quat_w, self.points_local) + object_pos_w if visualize: - environment_ids = torch.arange(env.num_envs, device=self.points_w.device).repeat_interleave( - self.points_w.shape[1] - ) - self.visualizer.visualize( - translations=self.points_w.view(-1, 3), - environment_ids=environment_ids, - ) - object_point_cloud_pos_b, _ = subtract_frame_transforms(ref_pos_w, ref_quat_w, self.points_w, None) - - return object_point_cloud_pos_b.view(env.num_envs, -1) if flatten else object_point_cloud_pos_b + self.visualizer.visualize(translations=self.points_w.view(-1, 3), environment_ids=self._marker_env_ids) + ref_pos_w = self.ref_asset.data.root_pos_w.torch.unsqueeze(1).expand(-1, num_points, -1) + ref_quat_w = self.ref_asset.data.root_quat_w.torch.unsqueeze(1).expand(-1, num_points, -1) + points_b, _ = subtract_frame_transforms(ref_pos_w, ref_quat_w, self.points_w) + return points_b.view(env.num_envs, -1) if flatten else points_b def fingers_contact_force_b( @@ -194,79 +168,52 @@ def fingers_contact_force_b( contact_sensor_names: list[str], asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: - """base-frame contact forces from listed sensors, concatenated per env. + """Contact forces [N] of the listed sensors in the robot root frame, concatenated per environment. Args: env: The environment. contact_sensor_names: Names of contact sensors in ``env.scene.sensors`` to read. + asset_cfg: Scene entity providing the root frame. Defaults to ``SceneEntityCfg("robot")``. Returns: Tensor of shape ``(num_envs, 3 * num_sensors)`` with forces stacked horizontally as ``[fx, fy, fz]`` per sensor. """ - force_w = [ - env.scene.sensors[name].data.normal_force_matrix_w.torch.view(env.num_envs, 3) for name in contact_sensor_names - ] - force_w = torch.stack(force_w, dim=1) + force_w = torch.stack( + [ + env.scene.sensors[name].data.normal_force_matrix_w.torch.view(env.num_envs, 3) + for name in contact_sensor_names + ], + dim=1, + ) robot: Articulation = env.scene[asset_cfg.name] - root_link_quat_w = robot.data.root_link_quat_w.torch - forces_b = quat_apply_inverse(root_link_quat_w.unsqueeze(1).repeat(1, force_w.shape[1], 1), force_w) - return forces_b.view(env.num_envs, -1) + root_quat_w = robot.data.root_link_quat_w.torch.unsqueeze(1).expand(-1, force_w.shape[1], -1) + return quat_apply_inverse(root_quat_w, force_w).view(env.num_envs, -1) class vision_camera(ManagerTermBase): - def __init__(self, cfg, env: ManagerBasedRLEnv): + """Normalized, channel-first camera images from a single-data-type camera sensor. + + RGB-like images are mapped to ``[-0.5, 0.5)``. Depth images are mapped onto the same span with + ``tanh(depth / 2) - 0.5``: a wider depth range would double the encoder's effective input scale + and halve the stable learning-rate budget. + """ + + def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): super().__init__(cfg, env) sensor_cfg: SceneEntityCfg = cfg.params.get("sensor_cfg", SceneEntityCfg("tiled_camera")) self.sensor: Camera = env.scene.sensors[sensor_cfg.name] self.sensor_type = self.sensor.cfg.data_types[0] - self.norm_fn = ( - self._depth_norm - if self.sensor_type == "distance_to_image_plane" or self.sensor_type == "depth" - else self._rgb_norm - ) + self._is_depth = self.sensor_type in ("distance_to_image_plane", "depth") - def __call__( - self, env: ManagerBasedRLEnv, sensor_cfg: SceneEntityCfg, normalize: bool = True - ) -> torch.Tensor: # obtain the input image + def __call__(self, env: ManagerBasedRLEnv, sensor_cfg: SceneEntityCfg, normalize: bool = True) -> torch.Tensor: images = self.sensor.data.output[self.sensor_type] torch.nan_to_num_(images, nan=1e6) if normalize: - images = self.norm_fn(images) + images = torch.tanh(images / 2) - 0.5 if self._is_depth else images.float() / 255.0 - 0.5 images = images.permute(0, 3, 1, 2).contiguous() return images - def _rgb_norm(self, images: torch.Tensor) -> torch.Tensor: - return images.float() / 255.0 - 0.5 - - def _depth_norm(self, images: torch.Tensor) -> torch.Tensor: - # same [-0.5, 0.5) span as the RGB normalization: a wider depth range doubles the - # encoder's effective input scale and halves the stable learning-rate budget - return torch.tanh(images / 2) - 0.5 - - def show_collage(self, images: torch.Tensor, save_path: str = "collage.png"): - import matplotlib - import numpy as np - from PIL import Image - - a = images.detach().cpu().numpy() - n, h, w, c = a.shape - s = int(np.ceil(np.sqrt(n))) - canvas = np.full((s * h, s * w, 3), 255, np.uint8) - turbo = matplotlib.colormaps["turbo"] - for i in range(n): - r, col = divmod(i, s) - img = a[i] - if c == 1: - d = img[..., 0] - d = (d - d.min()) / (np.ptp(d) + 1e-8) - rgb = (turbo(d)[..., :3] * 255).astype(np.uint8) - else: - x = img if img.max() > 1 else img * 255 - rgb = np.clip(x, 0, 255).astype(np.uint8) - canvas[r * h : (r + 1) * h, col * w : (col + 1) * w] = rgb - Image.fromarray(canvas).save(save_path) - def deformable_com_in_robot_root_frame( env: ManagerBasedRLEnv, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/rewards.py index 864c04be91b2..7c3e0d14e669 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/rewards.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Reward terms for the lift environments.""" + from __future__ import annotations import warnings @@ -12,8 +14,7 @@ import torch from isaaclab.managers import ManagerTermBase, RewardTermCfg, SceneEntityCfg -from isaaclab.utils import math as math_utils -from isaaclab.utils.math import combine_frame_transforms, compute_pose_error +from isaaclab.utils.math import combine_frame_transforms, compute_pose_error, quat_error_magnitude, quat_mul if TYPE_CHECKING: from isaaclab.assets import Articulation, CableObject, DeformableObject, RigidObject @@ -30,11 +31,10 @@ def object_ee_distance( object_cfg: SceneEntityCfg = SceneEntityCfg("object"), asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: - """Reward reaching the object using a tanh-kernel on end-effector distance with contact bonus. + """Reward reaching the object using a tanh kernel on the end-effector distance, scaled by contact. - The reward is close to 1 when the distance is small. The reward is scaled by contact: - - Full reward (1x) when good contact (thumb + finger) - - Reduced reward (0.1x) when no contact + The reward is close to 1 when the distance is small. It is paid in full while the thumb and at + least one finger touch the object, and scaled down to 0.1 otherwise. Args: env: The environment instance. @@ -54,12 +54,6 @@ def object_ee_distance( return (1 - torch.tanh(distance / std)) * contact_bonus -def _contact_force_mag(sensor: ContactSensor, num_envs: int) -> torch.Tensor: - """Extract per-environment contact force magnitude from a sensor's normal_force_matrix_w.""" - force = sensor.data.normal_force_matrix_w.torch.view(num_envs, 3) - return torch.linalg.norm(force, dim=-1) - - def contacts(env: ManagerBasedRLEnv, threshold: float, thumb_name: str, finger_names: list[str]) -> torch.Tensor: """Reward for good contact: thumb + at least one finger above threshold. @@ -112,12 +106,9 @@ class success_reward(ManagerTermBase): Maintains a sticky ``succeeded`` boolean tensor per environment that flips to ``True`` once the success condition is met during an episode and resets to ``False`` on environment reset. - Args: - cfg: Configuration object specifying term parameters. - env: The manager-based RL environment. """ - def __init__(self, cfg, env: ManagerBasedRLEnv): + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): super().__init__(cfg, env) self.succeeded = torch.zeros(env.num_envs, dtype=torch.bool, device=env.device) @@ -228,8 +219,8 @@ def orientation_command_error_tanh( asset: RigidObject = env.scene[asset_cfg.name] obj: RigidObject = env.scene[align_asset_cfg.name] command = env.command_manager.get_command(command_name) - des_quat_w = math_utils.quat_mul(asset.data.root_link_quat_w.torch, command[:, 3:7]) - quat_distance = math_utils.quat_error_magnitude(obj.data.root_quat_w.torch, des_quat_w) + des_quat_w = quat_mul(asset.data.root_link_quat_w.torch, command[:, 3:7]) + quat_distance = quat_error_magnitude(obj.data.root_quat_w.torch, des_quat_w) return (1 - torch.tanh(quat_distance / std)) * contacts(env, contact_threshold, thumb_name, finger_names).float() @@ -248,7 +239,7 @@ class _ProgressReward(ManagerTermBase): so it is re-seeded whenever the command resamples and never carries across goals. """ - def __init__(self, cfg, env: ManagerBasedRLEnv): + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): super().__init__(cfg, env) # inf marks an environment whose bar has not been seeded yet against its current command self.best_error = torch.full((env.num_envs,), float("inf"), device=env.device) @@ -363,8 +354,8 @@ def __call__( asset: RigidObject = env.scene[asset_cfg.name] obj: RigidObject = env.scene[align_asset_cfg.name] command = env.command_manager.get_command(command_name) - des_quat_w = math_utils.quat_mul(asset.data.root_link_quat_w.torch, command[:, 3:7]) - quat_distance = math_utils.quat_error_magnitude(obj.data.root_quat_w.torch, des_quat_w) + des_quat_w = quat_mul(asset.data.root_link_quat_w.torch, command[:, 3:7]) + quat_distance = quat_error_magnitude(obj.data.root_quat_w.torch, des_quat_w) gate = contacts(env, contact_threshold, thumb_name, finger_names) return self._progress(quat_distance, gate, min_improvement, command) @@ -412,24 +403,12 @@ def deformable_com_ee_distance( return 1.0 - torch.tanh(distance / std) -def _deformable_com_goal_metrics( - env: ManagerBasedRLEnv, - minimal_height: float, - command_name: str, - robot_cfg: SceneEntityCfg, - asset_cfg: SceneEntityCfg, -) -> tuple[torch.Tensor, torch.Tensor]: - """Compute deformable COM goal distance and lifted state.""" - robot: Articulation = env.scene[robot_cfg.name] - asset: DeformableObject = env.scene[asset_cfg.name] - command = env.command_manager.get_command(command_name) - des_pos_w, _ = combine_frame_transforms(robot.data.root_pos_w.torch, robot.data.root_quat_w.torch, command[:, :3]) - com_w = asset.data.root_pos_w.torch - return torch.linalg.norm(des_pos_w - com_w, dim=1), com_w[:, 2] > minimal_height - +class _GoalDistanceReward(ManagerTermBase): + """Base class for goal-distance rewards that also log the episode success rate. -class DeformableComGoalDistance(ManagerTermBase): - """Reward deformable COM goal tracking and log episode success.""" + Subclasses set :attr:`_succeeded` in ``__call__``; the flag is flushed to ``Metrics/success_rate`` + in ``extras["log"]`` on reset. + """ def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): super().__init__(cfg, env) @@ -441,6 +420,10 @@ def reset(self, env_ids: Sequence[int] | None = None) -> None: self._env.extras.setdefault("log", {})["Metrics/success_rate"] = self._succeeded[env_ids].float().mean().item() self._succeeded[env_ids] = False + +class DeformableComGoalDistance(_GoalDistanceReward): + """Reward deformable COM goal tracking and log episode success.""" + def __call__( self, env: ManagerBasedRLEnv, @@ -503,37 +486,9 @@ def cable_ee_distance( return 1.0 - torch.tanh(distance / std) -def _cable_segment_goal_metrics( - env: ManagerBasedRLEnv, - command_name: str, - segment_index: int, - robot_cfg: SceneEntityCfg, - asset_cfg: SceneEntityCfg, -) -> torch.Tensor: - """Compute cable segment goal distance.""" - robot: Articulation = env.scene[robot_cfg.name] - asset: CableObject = env.scene[asset_cfg.name] - command = env.command_manager.get_command(command_name) - desired_pos_w, _ = combine_frame_transforms( - robot.data.root_pos_w.torch, robot.data.root_quat_w.torch, command[:, :3] - ) - segment_pos_w = asset.data.segment_pose_w.torch[:, segment_index, :3] - return torch.linalg.norm(desired_pos_w - segment_pos_w, dim=1) - - -class CableSegmentGoalDistance(ManagerTermBase): +class CableSegmentGoalDistance(_GoalDistanceReward): """Reward cable segment goal tracking and log episode success.""" - def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): - super().__init__(cfg, env) - self._succeeded = torch.zeros(env.num_envs, dtype=torch.bool, device=env.device) - - def reset(self, env_ids: Sequence[int] | None = None) -> None: - if env_ids is None: - env_ids = slice(None) - self._env.extras.setdefault("log", {})["Metrics/success_rate"] = self._succeeded[env_ids].float().mean().item() - self._succeeded[env_ids] = False - def __call__( self, env: ManagerBasedRLEnv, @@ -560,3 +515,43 @@ def cable_segment_goal_reached( """Reward a cable segment for reaching the goal.""" distance = _cable_segment_goal_metrics(env, command_name, segment_index, robot_cfg, asset_cfg) return (distance < success_threshold).float() + + +def _contact_force_mag(sensor: ContactSensor, num_envs: int) -> torch.Tensor: + """Per-environment contact force magnitude [N] of a single-body, single-filter contact sensor.""" + force = sensor.data.normal_force_matrix_w.torch.view(num_envs, 3) + return torch.linalg.norm(force, dim=-1) + + +def _deformable_com_goal_metrics( + env: ManagerBasedRLEnv, + minimal_height: float, + command_name: str, + robot_cfg: SceneEntityCfg, + asset_cfg: SceneEntityCfg, +) -> tuple[torch.Tensor, torch.Tensor]: + """Compute deformable COM goal distance and lifted state.""" + robot: Articulation = env.scene[robot_cfg.name] + asset: DeformableObject = env.scene[asset_cfg.name] + command = env.command_manager.get_command(command_name) + des_pos_w, _ = combine_frame_transforms(robot.data.root_pos_w.torch, robot.data.root_quat_w.torch, command[:, :3]) + com_w = asset.data.root_pos_w.torch + return torch.linalg.norm(des_pos_w - com_w, dim=1), com_w[:, 2] > minimal_height + + +def _cable_segment_goal_metrics( + env: ManagerBasedRLEnv, + command_name: str, + segment_index: int, + robot_cfg: SceneEntityCfg, + asset_cfg: SceneEntityCfg, +) -> torch.Tensor: + """Compute cable segment goal distance.""" + robot: Articulation = env.scene[robot_cfg.name] + asset: CableObject = env.scene[asset_cfg.name] + command = env.command_manager.get_command(command_name) + desired_pos_w, _ = combine_frame_transforms( + robot.data.root_pos_w.torch, robot.data.root_quat_w.torch, command[:, :3] + ) + segment_pos_w = asset.data.segment_pose_w.torch[:, segment_index, :3] + return torch.linalg.norm(desired_pos_w - segment_pos_w, dim=1) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/terminations.py index 7745b21337bd..5f38e5d6ea9a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/terminations.py @@ -3,18 +3,13 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Common functions that can be used to activate certain terminations for the lift task. - -The functions can be passed to the :class:`isaaclab.managers.TerminationTermCfg` object to enable -the termination introduced by the function. -""" +"""Termination terms for the lift environments.""" from __future__ import annotations from typing import TYPE_CHECKING import torch -import warp as wp from isaaclab.managers import ManagerTermBase, SceneEntityCfg, TerminationTermCfg from isaaclab.utils.math import combine_frame_transforms @@ -49,8 +44,10 @@ def __call__( self, env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("object"), - in_bound_range: dict[str, tuple[float, float]] = {}, + in_bound_range: dict[str, tuple[float, float]] | None = None, ) -> torch.Tensor: + if in_bound_range is None: + in_bound_range = {} # rebuild only the axes whose bounds changed (curriculum typically only moves one) for i, key in enumerate(["x", "y", "z"]): bounds = tuple(in_bound_range.get(key, (0.0, 0.0))) @@ -65,8 +62,10 @@ def __call__( def abnormal_robot_state(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: - """Terminating environment when violation of velocity limits detects, this usually indicates unstable physics caused - by very bad, or aggressive action""" + """Terminate when a joint velocity exceeds twice its solver limit. + + Such violations indicate unstable physics, typically caused by aggressive actions. + """ robot: Articulation = env.scene[asset_cfg.name] joint_vel = robot.data.joint_vel.torch joint_vel_limits = robot.data.joint_vel_limits.torch @@ -95,7 +94,7 @@ def ee_below_minimum( ) -> torch.Tensor: """Return whether the end-effector is below the minimum environment-frame height [m].""" ee_frame: FrameTransformer = env.scene[ee_frame_cfg.name] - ee_z = wp.to_torch(ee_frame.data.target_pos_w)[..., 0, 2] - env.scene.env_origins[:, 2] + ee_z = ee_frame.data.target_pos_w.torch[..., 0, 2] - env.scene.env_origins[:, 2] return ee_z < minimum_height @@ -122,9 +121,7 @@ def deformable_outside_bounds( """ asset: DeformableObject = env.scene[asset_cfg.name] nodal_pos = asset.data.nodal_pos_w.torch - env.scene.env_origins.unsqueeze(1) - lower = torch.tensor([x_bounds[0], y_bounds[0], z_bounds[0]], device=nodal_pos.device) - upper = torch.tensor([x_bounds[1], y_bounds[1], z_bounds[1]], device=nodal_pos.device) - return ((nodal_pos < lower) | (nodal_pos > upper)).flatten(1).any(dim=1) + return _points_outside_box(nodal_pos, x_bounds, y_bounds, z_bounds) def cable_outside_bounds( @@ -134,12 +131,21 @@ def cable_outside_bounds( z_bounds: tuple[float, float], asset_cfg: SceneEntityCfg = SceneEntityCfg("cable"), ) -> torch.Tensor: - """Terminate if any cable segment leaves the allowed workspace box.""" + """Terminate if any cable segment leaves the allowed workspace box. + + Args: + env: The environment instance. + x_bounds: Allowed x-position range in the environment frame [m]. + y_bounds: Allowed y-position range in the environment frame [m]. + z_bounds: Allowed z-position range in the environment frame [m]. + asset_cfg: The cable entity. + + Returns: + Boolean tensor with shape ``(num_envs,)``. + """ asset: CableObject = env.scene[asset_cfg.name] segment_pos = asset.data.segment_pose_w.torch[..., :3] - env.scene.env_origins.unsqueeze(1) - lower = torch.tensor([x_bounds[0], y_bounds[0], z_bounds[0]], device=segment_pos.device) - upper = torch.tensor([x_bounds[1], y_bounds[1], z_bounds[1]], device=segment_pos.device) - return ((segment_pos < lower) | (segment_pos > upper)).flatten(1).any(dim=1) + return _points_outside_box(segment_pos, x_bounds, y_bounds, z_bounds) def joint_vel_out_of_sim_limit( @@ -151,3 +157,34 @@ def joint_vel_out_of_sim_limit( return torch.any( torch.abs(asset.data.joint_vel.torch[:, joint_ids]) > asset.data.joint_vel_limits.torch[:, joint_ids], dim=1 ) + + +def _points_outside_box( + points: torch.Tensor, + x_bounds: tuple[float, float], + y_bounds: tuple[float, float], + z_bounds: tuple[float, float], +) -> torch.Tensor: + """Return whether any of each environment's points [m] lies outside the axis-aligned box. + + The bounds are compared as Python scalars so the hot path allocates no constant tensors. + + Args: + points: Positions [m], shape ``(num_envs, num_points, 3)``. + x_bounds: Allowed x-position range [m]. + y_bounds: Allowed y-position range [m]. + z_bounds: Allowed z-position range [m]. + + Returns: + Boolean tensor with shape ``(num_envs,)``. + """ + x, y, z = points.unbind(dim=-1) + outside = ( + (x < x_bounds[0]) + | (x > x_bounds[1]) + | (y < y_bounds[0]) + | (y > y_bounds[1]) + | (z < z_bounds[0]) + | (z > z_bounds[1]) + ) + return outside.any(dim=1) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/utils.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/utils.py index 907f347f2689..d0ba2b24fdcd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/utils.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Geometry and reset-state helpers for the lift environments.""" + from __future__ import annotations import hashlib @@ -21,33 +23,41 @@ from isaaclab.envs import ManagerBasedEnv -# ---- module-scope caches ---- -_PRIM_SAMPLE_CACHE: dict[tuple[str, int], np.ndarray] = {} # (prim_hash, num_points) -> (N,3) in root frame -_FINAL_SAMPLE_CACHE: dict[str, np.ndarray] = {} # env_hash -> (num_points,3) in root frame +_PRIM_SAMPLE_CACHE: dict[tuple[str, int], np.ndarray] = {} +"""Per-prim surface samples in the object root frame, keyed by ``(prim_hash, num_points)``.""" + +_FINAL_SAMPLE_CACHE: dict[str, np.ndarray] = {} +"""Downsampled per-object samples in the object root frame, keyed by the object's geometry hash.""" -def clear_pointcloud_caches(): +def clear_pointcloud_caches() -> None: + """Clear the in-memory point-cloud sampling caches.""" _PRIM_SAMPLE_CACHE.clear() _FINAL_SAMPLE_CACHE.clear() def sample_object_point_cloud(num_envs: int, num_points: int, prim_path: str, device: str = "cpu") -> torch.Tensor: - """ - Samples point clouds for each environment instance by collecting points - from all matching USD prims under `prim_path`, then downsamples to - exactly `num_points` per env using farthest-point sampling. + """Sample a surface point cloud per environment from the USD prims matching ``prim_path``. - Caching is in-memory within this module: - - per-prim raw samples: _PRIM_SAMPLE_CACHE[(prim_hash, num_points)] - - final downsampled env: _FINAL_SAMPLE_CACHE[env_hash] + Points are gathered from every mesh or primitive-shape prim under each source prim of the clone + plan and downsampled to exactly ``num_points`` with farthest-point sampling. Samples are cached + in memory per prim geometry and per object geometry, so repeated calls for the same object + (across terms or environments) reuse them. + + Args: + num_envs: Number of environments. + num_points: Number of surface points per environment. + prim_path: Regex prim path of the object, e.g. ``"{ENV_REGEX_NS}/Object"``. + device: Device of the returned tensor. Returns: - torch.Tensor: Shape (num_envs, num_points, 3) on `device`. + Surface points [m] in the object root frame, shape ``(num_envs, num_points, 3)``. """ - import trimesh - from trimesh.sample import sample_surface + # USD and trimesh are runtime dependencies that must not load at config-import time + import trimesh # noqa: PLC0415 + from trimesh.sample import sample_surface # noqa: PLC0415 - from pxr import UsdGeom + from pxr import UsdGeom # noqa: PLC0415 points = torch.zeros((num_envs, num_points, 3), dtype=torch.float32, device=device) xform_cache = UsdGeom.XformCache() @@ -188,7 +198,7 @@ def sample_object_point_cloud(num_envs: int, num_points: int, prim_path: str, de def _triangulate_faces(prim) -> np.ndarray: """Convert a USD Mesh prim into triangulated face indices (N, 3).""" - from pxr import UsdGeom + from pxr import UsdGeom # noqa: PLC0415 mesh = UsdGeom.Mesh(prim) counts = mesh.GetFaceVertexCountsAttr().Get() @@ -202,11 +212,11 @@ def _triangulate_faces(prim) -> np.ndarray: return np.asarray(faces, dtype=np.int64) -def create_primitive_mesh(prim): +def create_primitive_mesh(prim) -> trimesh.Trimesh: """Create a trimesh mesh from a USD primitive (Cube, Sphere, Cylinder, etc.).""" - import trimesh + import trimesh # noqa: PLC0415 - from pxr import UsdGeom + from pxr import UsdGeom # noqa: PLC0415 prim_type = prim.GetTypeName() if prim_type == "Cube": @@ -221,7 +231,7 @@ def create_primitive_mesh(prim): elif prim_type == "Capsule": c = UsdGeom.Capsule(prim) return trimesh.creation.capsule(radius=c.GetRadiusAttr().Get(), height=c.GetHeightAttr().Get()) - elif prim_type == "Cone": # Cone + elif prim_type == "Cone": c = UsdGeom.Cone(prim) return trimesh.creation.cone(radius=c.GetRadiusAttr().Get(), height=c.GetHeightAttr().Get()) else: @@ -277,12 +287,25 @@ def farthest_point_sampling( def collect_collision_meshes(root_prim, owner_frame_fn: Callable) -> dict[int, trimesh.Trimesh]: - """Collect collision meshes under ``root_prim``, grouped in caller-selected frames.""" - import trimesh + """Collect collision meshes under ``root_prim``, grouped in caller-selected frames. - from pxr import UsdPhysics + Args: + root_prim: Prim whose subtree is searched for collision meshes. + owner_frame_fn: Maps a collision prim to ``(owner_key, frame_prim)``, or ``None`` to skip it. The + mesh is expressed in ``frame_prim``'s frame and merged with other meshes of the same owner. - from isaaclab.utils.mesh import PRIMITIVE_MESH_TYPES, create_trimesh_from_geom_mesh, create_trimesh_from_geom_shape + Returns: + One merged mesh per owner key. + """ + import trimesh # noqa: PLC0415 + + from pxr import UsdPhysics # noqa: PLC0415 + + from isaaclab.utils.mesh import ( # noqa: PLC0415 + PRIMITIVE_MESH_TYPES, + create_trimesh_from_geom_mesh, + create_trimesh_from_geom_shape, + ) mesh_types = PRIMITIVE_MESH_TYPES + ["Mesh"] mesh_prims = sim_utils.get_all_matching_child_prims( diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/__init__.py index f5cb444ca7c4..1b8aa26bc4d8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/__init__.py @@ -3,6 +3,4 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Locomotion task family (ant and humanoid). -""" +"""Locomotion task family (ant and humanoid).""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/__init__.py index fe352c446ace..5b839d735969 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/__init__.py @@ -3,17 +3,15 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Ant locomotion environment (similar to OpenAI Gym Ant-v2). +"""Ant locomotion environments (similar to OpenAI Gym Ant-v2). -This package consolidates the direct-workflow and manager-based-workflow ant -tasks. Module files carry a ``_direct_`` or ``_manager_`` infix to disambiguate -the two workflows within the flat package layout. +This package consolidates the direct-workflow and manager-based-workflow ant tasks. Module files carry +a ``_direct_`` or ``_manager_`` infix to disambiguate the two workflows within the flat package layout. """ import gymnasium as gym -from isaaclab_tasks.core.locomotion.ant import agents +from . import agents ## # Register Gym environments -- direct workflow. @@ -42,9 +40,9 @@ disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.ant_manager_env_cfg:AntEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_manager_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AntPPORunnerCfg", "default_agent": "rsl_rl", - "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_manager_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_manager_ppo_cfg.yaml", "sb3_cfg_entry_point": f"{agents.__name__}:sb3_manager_ppo_cfg.yaml", }, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_common.py new file mode 100644 index 000000000000..f0bc52300997 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_common.py @@ -0,0 +1,67 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configurations shared by the direct and manager-based Ant environments.""" + +from isaaclab_newton.physics import KaminoPADMMSolverCfg, MJWarpSolverCfg, NewtonCfg +from isaaclab_ov.physics import OvPhysxCfg +from isaaclab_physx.physics import PhysxCfg + +import isaaclab.sim as sim_utils +from isaaclab.physics import PhysxAutoCfg +from isaaclab.terrains import TerrainImporterCfg +from isaaclab.utils import configclass + +from isaaclab_tasks.utils import PresetCfg + +TERRAIN_CFG = TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="plane", + collision_group=-1, + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="average", + restitution_combine_mode="average", + static_friction=1.0, + dynamic_friction=1.0, + restitution=0.0, + ), + debug_vis=False, +) +"""Flat ground plane the Ant walks on.""" + +JOINT_GEARS: dict[str, float] = {".*": 15.0} +"""Effort scale per joint [N·m], keyed by joint name expression.""" + +FEET_BODY_NAMES: list[str] = ["front_left_foot", "front_right_foot", "left_back_foot", "right_back_foot"] +"""Bodies whose incoming wrench is observed.""" + +WALK_TARGET_POS: tuple[float, float, float] = (1000.0, 0.0, 0.0) +"""Walk target [m] relative to the environment origin, far enough away that it is never reached.""" + + +@configclass +class AntPhysicsCfg(PresetCfg): + """Physics backend presets for the Ant environments.""" + + isaacsim_physx: PhysxCfg = PhysxCfg(bounce_threshold_velocity=0.2) + ovphysx: OvPhysxCfg = OvPhysxCfg() + physx: PhysxAutoCfg = PhysxAutoCfg(isaacsim_physx=isaacsim_physx, ovphysx=ovphysx) + newton_mjwarp: NewtonCfg = NewtonCfg( + solver_cfg=MJWarpSolverCfg( + njmax=45, + nconmax=25, + cone="pyramidal", + integrator="implicitfast", + impratio=1, + ), + num_substeps=1, + debug_mode=False, + ) + newton_kamino: NewtonCfg = NewtonCfg( + solver_cfg=KaminoPADMMSolverCfg(sparse_jacobian=True), + debug_mode=False, + use_cuda_graph=True, + ) + default: NewtonCfg = newton_mjwarp diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env.py index a2b647f6c0b1..0c2ff59d38d8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Direct-workflow Ant locomotion environment.""" + from __future__ import annotations from isaaclab_tasks.core.locomotion.ant.ant_direct_env_cfg import AntEnvCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env_cfg.py index 9719312fdf43..cb75488588b1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env_cfg.py @@ -3,72 +3,34 @@ # # SPDX-License-Identifier: BSD-3-Clause -from __future__ import annotations +"""Configuration for the direct-workflow Ant environment.""" -from isaaclab_newton.physics import ( - KaminoPADMMSolverCfg, - MJWarpSolverCfg, - NewtonCfg, -) -from isaaclab_ov.physics import OvPhysxCfg -from isaaclab_physx.physics import PhysxCfg +from __future__ import annotations import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, AssetBaseCfg from isaaclab.envs import DirectRLEnvCfg -from isaaclab.physics import PhysxAutoCfg from isaaclab.scene import InteractiveSceneCfg from isaaclab.sensors import JointWrenchSensorCfg from isaaclab.sim import SimulationCfg -from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass -from isaaclab_tasks.utils import PresetCfg +from isaaclab_tasks.core.locomotion.ant.ant_common import ( + FEET_BODY_NAMES, + JOINT_GEARS, + TERRAIN_CFG, + WALK_TARGET_POS, + AntPhysicsCfg, +) from isaaclab_assets.robots.ant import ANT_CFG -@configclass -class AntPhysicsCfg(PresetCfg): - isaacsim_physx: PhysxCfg = PhysxCfg(bounce_threshold_velocity=0.2) - ovphysx: OvPhysxCfg = OvPhysxCfg() - physx: PhysxAutoCfg = PhysxAutoCfg(isaacsim_physx=isaacsim_physx, ovphysx=ovphysx) - newton_mjwarp: NewtonCfg = NewtonCfg( - solver_cfg=MJWarpSolverCfg( - njmax=45, - nconmax=25, - cone="pyramidal", - integrator="implicitfast", - impratio=1, - ), - num_substeps=1, - debug_mode=False, - ) - newton_kamino: NewtonCfg = NewtonCfg( - solver_cfg=KaminoPADMMSolverCfg(sparse_jacobian=True), - debug_mode=False, - use_cuda_graph=True, - ) - default = newton_mjwarp - - @configclass class AntDirectSceneCfg(InteractiveSceneCfg): """Ant, terrain, sensor, and light constructed through one clone lifecycle.""" - terrain = TerrainImporterCfg( - prim_path="/World/ground", - terrain_type="plane", - collision_group=-1, - physics_material=sim_utils.RigidBodyMaterialCfg( - friction_combine_mode="average", - restitution_combine_mode="average", - static_friction=1.0, - dynamic_friction=1.0, - restitution=0.0, - ), - debug_vis=False, - ) + terrain = TERRAIN_CFG robot: ArticulationCfg = ANT_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") joint_wrench = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot") light = AssetBaseCfg( @@ -96,29 +58,29 @@ class AntEnvCfg(DirectRLEnvCfg): num_envs=4096, env_spacing=5.0, replicate_physics=True, clone_in_fabric=True ) - # effort scale per joint, keyed by joint name expression - joint_gears: dict[str, float] = {".*": 15.0} - - feet_body_names: list[str] = ["front_left_foot", "front_right_foot", "left_back_foot", "right_back_foot"] + # robot + joint_gears: dict[str, float] = JOINT_GEARS + feet_body_names: list[str] = FEET_BODY_NAMES # walk target, relative to the environment origin - target_pos: tuple[float, float, float] = (1000.0, 0.0, 0.0) + target_pos: tuple[float, float, float] = WALK_TARGET_POS # reset initial_joint_pos_range: tuple[float, float] = (-0.2, 0.2) # [rad] initial_joint_vel_range: tuple[float, float] = (-0.1, 0.1) # [rad/s] + # reward scales heading_weight: float = 0.5 up_weight: float = 0.1 - energy_cost_scale: float = 0.05 actions_cost_scale: float = 0.005 alive_reward_scale: float = 0.5 joint_pos_limits_cost_scale: float = 0.1 joint_pos_limits_threshold: float = 0.99 - death_cost: float = -2.0 - termination_height: float = 0.31 + + # termination + termination_height: float = 0.31 # [m] # observation scales dof_vel_scale: float = 0.2 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_manager_env_cfg.py index 9140e0ee3041..44ebace6aca4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_manager_env_cfg.py @@ -3,13 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -from isaaclab_newton.physics import ( - KaminoPADMMSolverCfg, - MJWarpSolverCfg, - NewtonCfg, -) -from isaaclab_ov.physics import OvPhysxCfg -from isaaclab_physx.physics import PhysxCfg +"""Configuration for the manager-based Ant environment.""" import isaaclab.sim as sim_utils from isaaclab.assets import AssetBaseCfg @@ -20,40 +14,24 @@ from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.managers import SceneEntityCfg from isaaclab.managers import TerminationTermCfg as DoneTerm -from isaaclab.physics import PhysxAutoCfg from isaaclab.scene import InteractiveSceneCfg from isaaclab.sensors import JointWrenchSensorCfg -from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass import isaaclab_tasks.core.locomotion.mdp as mdp -from isaaclab_tasks.utils import PresetCfg +from isaaclab_tasks.core.locomotion.ant.ant_common import ( + FEET_BODY_NAMES, + JOINT_GEARS, + TERRAIN_CFG, + WALK_TARGET_POS, + AntPhysicsCfg, +) from isaaclab_assets.robots.ant import ANT_CFG - -@configclass -class AntPhysicsCfg(PresetCfg): - isaacsim_physx: PhysxCfg = PhysxCfg(bounce_threshold_velocity=0.2) - ovphysx: OvPhysxCfg = OvPhysxCfg() - physx: PhysxAutoCfg = PhysxAutoCfg(isaacsim_physx=isaacsim_physx, ovphysx=ovphysx) - newton_mjwarp: NewtonCfg = NewtonCfg( - solver_cfg=MJWarpSolverCfg( - njmax=45, - nconmax=25, - cone="pyramidal", - integrator="implicitfast", - impratio=1, - ), - num_substeps=1, - debug_mode=False, - ) - newton_kamino: NewtonCfg = NewtonCfg( - solver_cfg=KaminoPADMMSolverCfg(sparse_jacobian=True), - debug_mode=False, - use_cuda_graph=True, - ) - default: NewtonCfg = newton_mjwarp +## +# Scene definition +## @configclass @@ -61,19 +39,7 @@ class AntSceneCfg(InteractiveSceneCfg): """Configuration for the terrain scene with an ant robot.""" # terrain - terrain = TerrainImporterCfg( - prim_path="/World/ground", - terrain_type="plane", - collision_group=-1, - physics_material=sim_utils.RigidBodyMaterialCfg( - friction_combine_mode="average", - restitution_combine_mode="average", - static_friction=1.0, - dynamic_friction=1.0, - restitution=0.0, - ), - debug_vis=False, - ) + terrain = TERRAIN_CFG # robot robot = ANT_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") @@ -114,20 +80,15 @@ class PolicyCfg(ObsGroup): base_lin_vel = ObsTerm(func=mdp.base_lin_vel) base_ang_vel = ObsTerm(func=mdp.base_ang_vel) base_yaw_roll = ObsTerm(func=mdp.base_yaw_roll) - base_angle_to_target = ObsTerm(func=mdp.base_angle_to_target, params={"target_pos": (1000.0, 0.0, 0.0)}) + base_angle_to_target = ObsTerm(func=mdp.base_angle_to_target, params={"target_pos": WALK_TARGET_POS}) base_up_proj = ObsTerm(func=mdp.base_up_proj) - base_heading_proj = ObsTerm(func=mdp.base_heading_proj, params={"target_pos": (1000.0, 0.0, 0.0)}) + base_heading_proj = ObsTerm(func=mdp.base_heading_proj, params={"target_pos": WALK_TARGET_POS}) joint_pos_norm = ObsTerm(func=mdp.joint_pos_limit_normalized) joint_vel_rel = ObsTerm(func=mdp.joint_vel_rel, scale=0.2) feet_body_forces = ObsTerm( func=mdp.body_incoming_wrench, scale=0.1, - params={ - "sensor_cfg": SceneEntityCfg( - "joint_wrench", - body_names=["front_left_foot", "front_right_foot", "left_back_foot", "right_back_foot"], - ) - }, + params={"sensor_cfg": SceneEntityCfg("joint_wrench", body_names=FEET_BODY_NAMES)}, ) actions = ObsTerm(func=mdp.last_action) @@ -139,14 +100,6 @@ def __post_init__(self): policy: PolicyCfg = PolicyCfg() -@configclass -class AntObservationsCfg(PresetCfg): - physx: ObservationsCfg = ObservationsCfg() - isaacsim_physx: ObservationsCfg = physx - newton_mjwarp: ObservationsCfg = ObservationsCfg() - default: ObservationsCfg = newton_mjwarp - - @configclass class EventCfg: """Configuration for events.""" @@ -172,22 +125,22 @@ class RewardsCfg: """Reward terms for the MDP.""" # (1) Reward for moving forward - progress = RewTerm(func=mdp.progress_reward, weight=1.0, params={"target_pos": (1000.0, 0.0, 0.0)}) + progress = RewTerm(func=mdp.progress_reward, weight=1.0, params={"target_pos": WALK_TARGET_POS}) # (2) Stay alive bonus alive = RewTerm(func=mdp.is_alive, weight=0.5) # (3) Reward for upright posture upright = RewTerm(func=mdp.upright_posture_bonus, weight=0.1, params={"threshold": 0.93}) # (4) Reward for moving in the right direction move_to_target = RewTerm( - func=mdp.move_to_target_bonus, weight=0.5, params={"threshold": 0.8, "target_pos": (1000.0, 0.0, 0.0)} + func=mdp.move_to_target_bonus, weight=0.5, params={"threshold": 0.8, "target_pos": WALK_TARGET_POS} ) # (5) Penalty for large action commands action_l2 = RewTerm(func=mdp.action_l2, weight=-0.005) # (6) Penalty for energy consumption - energy = RewTerm(func=mdp.power_consumption, weight=-0.05, params={"gear_ratio": {".*": 15.0}}) + energy = RewTerm(func=mdp.power_consumption, weight=-0.05, params={"gear_ratio": JOINT_GEARS}) # (7) Penalty for reaching close to joint limits joint_pos_limits = RewTerm( - func=mdp.joint_pos_limits_penalty_ratio, weight=-0.1, params={"threshold": 0.99, "gear_ratio": {".*": 15.0}} + func=mdp.joint_pos_limits_penalty_ratio, weight=-0.1, params={"threshold": 0.99, "gear_ratio": JOINT_GEARS} ) # (8) Penalty for falling over, applied once on the terminating step terminating = RewTerm(func=mdp.terminated_penalty, weight=-2.0) @@ -205,14 +158,19 @@ class TerminationsCfg: torso_height = DoneTerm(func=mdp.root_height_below_minimum, params={"minimum_height": 0.31}) +## +# Environment configuration +## + + @configclass class AntEnvCfg(ManagerBasedRLEnvCfg): - """Configuration for the Ant walking environment.""" + """Configuration for the manager-based Ant walking environment.""" # Scene settings scene: AntSceneCfg = AntSceneCfg(num_envs=4096, env_spacing=5.0, clone_in_fabric=True) # Basic settings - observations: AntObservationsCfg = AntObservationsCfg() + observations: ObservationsCfg = ObservationsCfg() actions: ActionsCfg = ActionsCfg() # MDP settings rewards: RewardsCfg = RewardsCfg() @@ -225,10 +183,9 @@ def __post_init__(self): self.decimation = 2 self.episode_length_s = 16.0 # simulation settings - self.sim.dt = 1 / 120.0 + self.sim.dt = 1 / 120 self.sim.render_interval = self.decimation self.sim.physics = AntPhysicsCfg() - # default friction material self.sim.physics_material.static_friction = 1.0 self.sim.physics_material.dynamic_friction = 1.0 self.sim.physics_material.restitution = 0.0 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/__init__.py index 1c43bd1e3fcb..4abd9ba74be6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/__init__.py @@ -3,17 +3,16 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Humanoid locomotion environment (similar to OpenAI Gym Humanoid-v2). +"""Humanoid locomotion environments (similar to OpenAI Gym Humanoid-v2). -This package consolidates the direct-workflow and manager-based-workflow humanoid tasks. Module -files carry a ``_direct_`` or ``_manager_`` infix to disambiguate the two workflows within the flat -package layout. +This package consolidates the direct-workflow and manager-based-workflow humanoid tasks. Module files +carry a ``_direct_`` or ``_manager_`` infix to disambiguate the two workflows within the flat package +layout. """ import gymnasium as gym -from isaaclab_tasks.core.locomotion.humanoid import agents +from . import agents ## # Register Gym environments -- direct workflow. @@ -42,9 +41,9 @@ disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.humanoid_manager_env_cfg:HumanoidEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_manager_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:HumanoidPPORunnerCfg", "default_agent": "rsl_rl", - "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_manager_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_manager_ppo_cfg.yaml", "sb3_cfg_entry_point": f"{agents.__name__}:sb3_manager_ppo_cfg.yaml", }, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_common.py new file mode 100644 index 000000000000..e08f7e9bebb0 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_common.py @@ -0,0 +1,59 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configurations shared by the direct and manager-based Humanoid environments.""" + +from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg +from isaaclab_ov.physics import OvPhysxCfg +from isaaclab_physx.physics import PhysxCfg + +from isaaclab.physics import PhysxAutoCfg +from isaaclab.utils import configclass + +from isaaclab_tasks.utils import PresetCfg + +JOINT_GEARS: dict[str, float] = { + ".*_waist.*": 67.5, + ".*_upper_arm.*": 67.5, + "pelvis": 67.5, + ".*_lower_arm": 45.0, + ".*_thigh:0": 45.0, + ".*_thigh:1": 135.0, + ".*_thigh:2": 45.0, + ".*_shin": 90.0, + ".*_foot.*": 22.5, +} +"""Effort scale per joint [N·m], keyed by joint name expression.""" + +JOINT_EFFORT_LIMITS = {name: (-gear, gear) for name, gear in JOINT_GEARS.items()} +"""Effort clip per joint [N·m], i.e. the effort produced by a unit action.""" + +FEET_BODY_NAMES: list[str] = ["left_foot", "right_foot"] +"""Bodies whose incoming wrench is observed.""" + +WALK_TARGET_POS: tuple[float, float, float] = (1000.0, 0.0, 0.0) +"""Walk target [m] relative to the environment origin, far enough away that it is never reached.""" + + +@configclass +class HumanoidPhysicsCfg(PresetCfg): + """Physics backend presets for the Humanoid environments.""" + + isaacsim_physx: PhysxCfg = PhysxCfg(bounce_threshold_velocity=0.2) + ovphysx: OvPhysxCfg = OvPhysxCfg() + physx: PhysxAutoCfg = PhysxAutoCfg(isaacsim_physx=isaacsim_physx, ovphysx=ovphysx) + newton_mjwarp: NewtonCfg = NewtonCfg( + solver_cfg=MJWarpSolverCfg( + njmax=80, + nconmax=25, + cone="pyramidal", + update_data_interval=2, + integrator="implicitfast", + impratio=1, + ), + num_substeps=2, + debug_mode=False, + ) + default: NewtonCfg = newton_mjwarp diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env.py index 7e4780eeb351..3214023e6b42 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Direct-workflow Humanoid locomotion environment.""" + from __future__ import annotations from isaaclab_tasks.core.locomotion.humanoid.humanoid_direct_env_cfg import HumanoidEnvCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env_cfg.py index 9bd2f039e1d3..17216a9d8469 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env_cfg.py @@ -3,64 +3,34 @@ # # SPDX-License-Identifier: BSD-3-Clause -from __future__ import annotations +"""Configuration for the direct-workflow Humanoid environment.""" -from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg -from isaaclab_ov.physics import OvPhysxCfg -from isaaclab_physx.physics import PhysxCfg +from __future__ import annotations import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, AssetBaseCfg from isaaclab.envs import DirectRLEnvCfg -from isaaclab.physics import PhysxAutoCfg from isaaclab.scene import InteractiveSceneCfg from isaaclab.sensors import JointWrenchSensorCfg from isaaclab.sim import SimulationCfg -from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass -from isaaclab_tasks.utils import PresetCfg +from isaaclab_tasks.core.locomotion.ant.ant_common import TERRAIN_CFG +from isaaclab_tasks.core.locomotion.humanoid.humanoid_common import ( + FEET_BODY_NAMES, + JOINT_GEARS, + WALK_TARGET_POS, + HumanoidPhysicsCfg, +) from isaaclab_assets.robots.humanoid import HUMANOID_CFG -@configclass -class HumanoidPhysicsCfg(PresetCfg): - isaacsim_physx: PhysxCfg = PhysxCfg(bounce_threshold_velocity=0.2) - ovphysx: OvPhysxCfg = OvPhysxCfg() - physx: PhysxAutoCfg = PhysxAutoCfg(isaacsim_physx=isaacsim_physx, ovphysx=ovphysx) - newton_mjwarp: NewtonCfg = NewtonCfg( - solver_cfg=MJWarpSolverCfg( - njmax=80, - nconmax=25, - cone="pyramidal", - update_data_interval=2, - integrator="implicitfast", - impratio=1, - ), - num_substeps=2, - debug_mode=False, - ) - default = newton_mjwarp - - @configclass class HumanoidDirectSceneCfg(InteractiveSceneCfg): """Humanoid, terrain, sensor, and light constructed through one clone lifecycle.""" - terrain = TerrainImporterCfg( - prim_path="/World/ground", - terrain_type="plane", - collision_group=-1, - physics_material=sim_utils.RigidBodyMaterialCfg( - friction_combine_mode="average", - restitution_combine_mode="average", - static_friction=1.0, - dynamic_friction=1.0, - restitution=0.0, - ), - debug_vis=False, - ) + terrain = TERRAIN_CFG robot: ArticulationCfg = HUMANOID_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") joint_wrench = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot") light = AssetBaseCfg( @@ -88,39 +58,29 @@ class HumanoidEnvCfg(DirectRLEnvCfg): num_envs=4096, env_spacing=5.0, replicate_physics=True, clone_in_fabric=True ) - # effort scale per joint, keyed by joint name expression - joint_gears: dict[str, float] = { - ".*_waist.*": 67.5, - ".*_upper_arm.*": 67.5, - "pelvis": 67.5, - ".*_lower_arm": 45.0, - ".*_thigh:0": 45.0, - ".*_thigh:1": 135.0, - ".*_thigh:2": 45.0, - ".*_shin": 90.0, - ".*_foot.*": 22.5, - } - - feet_body_names: list[str] = ["left_foot", "right_foot"] + # robot + joint_gears: dict[str, float] = JOINT_GEARS + feet_body_names: list[str] = FEET_BODY_NAMES # walk target, relative to the environment origin - target_pos: tuple[float, float, float] = (1000.0, 0.0, 0.0) + target_pos: tuple[float, float, float] = WALK_TARGET_POS # reset initial_joint_pos_range: tuple[float, float] = (-0.2, 0.2) # [rad] initial_joint_vel_range: tuple[float, float] = (-0.1, 0.1) # [rad/s] + # reward scales heading_weight: float = 0.5 up_weight: float = 0.1 - energy_cost_scale: float = 0.005 actions_cost_scale: float = 0.01 alive_reward_scale: float = 2.0 joint_pos_limits_cost_scale: float = 0.25 joint_pos_limits_threshold: float = 0.98 - death_cost: float = -1.0 - termination_height: float = 0.8 + + # termination + termination_height: float = 0.8 # [m] # observation scales dof_vel_scale: float = 0.1 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_manager_env_cfg.py index 3a7090934c06..d9d2acc1a12f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_manager_env_cfg.py @@ -3,9 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg -from isaaclab_ov.physics import OvPhysxCfg -from isaaclab_physx.physics import PhysxCfg +"""Configuration for the manager-based Humanoid environment.""" import isaaclab.sim as sim_utils from isaaclab.assets import AssetBaseCfg @@ -16,54 +14,22 @@ from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.managers import SceneEntityCfg from isaaclab.managers import TerminationTermCfg as DoneTerm -from isaaclab.physics import PhysxAutoCfg from isaaclab.scene import InteractiveSceneCfg from isaaclab.sensors import JointWrenchSensorCfg from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass import isaaclab_tasks.core.locomotion.mdp as mdp -from isaaclab_tasks.utils import PresetCfg +from isaaclab_tasks.core.locomotion.humanoid.humanoid_common import ( + FEET_BODY_NAMES, + JOINT_EFFORT_LIMITS, + JOINT_GEARS, + WALK_TARGET_POS, + HumanoidPhysicsCfg, +) from isaaclab_assets.robots.humanoid import HUMANOID_CFG -JOINT_GEARS = { - ".*_waist.*": 67.5, - ".*_upper_arm.*": 67.5, - "pelvis": 67.5, - ".*_lower_arm": 45.0, - ".*_thigh:0": 45.0, - ".*_thigh:1": 135.0, - ".*_thigh:2": 45.0, - ".*_shin": 90.0, - ".*_foot.*": 22.5, -} -"""Effort scale per joint [N·m], keyed by joint name expression.""" - -JOINT_EFFORT_LIMITS = {name: (-gear, gear) for name, gear in JOINT_GEARS.items()} -"""Effort clip per joint [N·m], i.e. the effort produced by a unit action.""" - - -@configclass -class HumanoidPhysicsCfg(PresetCfg): - isaacsim_physx: PhysxCfg = PhysxCfg(bounce_threshold_velocity=0.2) - ovphysx: OvPhysxCfg = OvPhysxCfg() - physx: PhysxAutoCfg = PhysxAutoCfg(isaacsim_physx=isaacsim_physx, ovphysx=ovphysx) - newton_mjwarp: NewtonCfg = NewtonCfg( - solver_cfg=MJWarpSolverCfg( - njmax=80, - nconmax=25, - cone="pyramidal", - update_data_interval=2, - integrator="implicitfast", - impratio=1, - ), - num_substeps=2, - debug_mode=False, - ) - default: NewtonCfg = newton_mjwarp - - ## # Scene definition ## @@ -123,15 +89,15 @@ class PolicyCfg(ObsGroup): base_lin_vel = ObsTerm(func=mdp.base_lin_vel) base_ang_vel = ObsTerm(func=mdp.base_ang_vel, scale=0.25) base_yaw_roll = ObsTerm(func=mdp.base_yaw_roll) - base_angle_to_target = ObsTerm(func=mdp.base_angle_to_target, params={"target_pos": (1000.0, 0.0, 0.0)}) + base_angle_to_target = ObsTerm(func=mdp.base_angle_to_target, params={"target_pos": WALK_TARGET_POS}) base_up_proj = ObsTerm(func=mdp.base_up_proj) - base_heading_proj = ObsTerm(func=mdp.base_heading_proj, params={"target_pos": (1000.0, 0.0, 0.0)}) + base_heading_proj = ObsTerm(func=mdp.base_heading_proj, params={"target_pos": WALK_TARGET_POS}) joint_pos_norm = ObsTerm(func=mdp.joint_pos_limit_normalized) joint_vel_rel = ObsTerm(func=mdp.joint_vel_rel, scale=0.1) feet_body_forces = ObsTerm( func=mdp.body_incoming_wrench, scale=0.01, - params={"sensor_cfg": SceneEntityCfg("joint_wrench", body_names=["left_foot", "right_foot"])}, + params={"sensor_cfg": SceneEntityCfg("joint_wrench", body_names=FEET_BODY_NAMES)}, ) actions = ObsTerm(func=mdp.last_action) @@ -143,14 +109,6 @@ def __post_init__(self): policy: PolicyCfg = PolicyCfg() -@configclass -class HumanoidObservationsCfg(PresetCfg): - physx: ObservationsCfg = ObservationsCfg() - isaacsim_physx: ObservationsCfg = physx - newton_mjwarp: ObservationsCfg = ObservationsCfg() - default: ObservationsCfg = newton_mjwarp - - @configclass class EventCfg: """Configuration for events.""" @@ -176,14 +134,14 @@ class RewardsCfg: """Reward terms for the MDP.""" # (1) Reward for moving forward - progress = RewTerm(func=mdp.progress_reward, weight=1.0, params={"target_pos": (1000.0, 0.0, 0.0)}) + progress = RewTerm(func=mdp.progress_reward, weight=1.0, params={"target_pos": WALK_TARGET_POS}) # (2) Stay alive bonus alive = RewTerm(func=mdp.is_alive, weight=2.0) # (3) Reward for upright posture upright = RewTerm(func=mdp.upright_posture_bonus, weight=0.1, params={"threshold": 0.93}) # (4) Reward for moving in the right direction move_to_target = RewTerm( - func=mdp.move_to_target_bonus, weight=0.5, params={"threshold": 0.8, "target_pos": (1000.0, 0.0, 0.0)} + func=mdp.move_to_target_bonus, weight=0.5, params={"threshold": 0.8, "target_pos": WALK_TARGET_POS} ) # (5) Penalty for large action commands action_l2 = RewTerm(func=mdp.action_l2, weight=-0.01) @@ -211,14 +169,19 @@ class TerminationsCfg: torso_height = DoneTerm(func=mdp.root_height_below_minimum, params={"minimum_height": 0.8}) +## +# Environment configuration +## + + @configclass class HumanoidEnvCfg(ManagerBasedRLEnvCfg): - """Configuration for the Humanoid walking environment.""" + """Configuration for the manager-based Humanoid walking environment.""" # Scene settings scene: HumanoidSceneCfg = HumanoidSceneCfg(num_envs=4096, env_spacing=5.0, clone_in_fabric=True) # Basic settings - observations: HumanoidObservationsCfg = HumanoidObservationsCfg() + observations: ObservationsCfg = ObservationsCfg() actions: ActionsCfg = ActionsCfg() # MDP settings rewards: RewardsCfg = RewardsCfg() @@ -231,10 +194,9 @@ def __post_init__(self): self.decimation = 2 self.episode_length_s = 16.0 # simulation settings - self.sim.dt = 1 / 120.0 + self.sim.dt = 1 / 120 self.sim.render_interval = self.decimation self.sim.physics = HumanoidPhysicsCfg() - # default friction material self.sim.physics_material.static_friction = 1.0 self.sim.physics_material.dynamic_friction = 1.0 self.sim.physics_material.restitution = 0.0 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/locomotion_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/locomotion_direct_env.py index fab3f6126192..b614f235cd7e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/locomotion_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/locomotion_direct_env.py @@ -3,13 +3,16 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Direct-workflow locomotion environment shared by the Ant and Humanoid tasks.""" + from __future__ import annotations from collections.abc import Sequence +from typing import TYPE_CHECKING import torch -from isaaclab.envs import DirectRLEnv, DirectRLEnvCfg +from isaaclab.envs import DirectRLEnv from isaaclab.utils.math import ( euler_xyz_from_quat, normalize, @@ -20,6 +23,10 @@ ) from isaaclab.utils.string import resolve_matching_names_values +if TYPE_CHECKING: + from isaaclab_tasks.core.locomotion.ant.ant_direct_env_cfg import AntEnvCfg + from isaaclab_tasks.core.locomotion.humanoid.humanoid_direct_env_cfg import HumanoidEnvCfg + class LocomotionDirectEnv(DirectRLEnv): """Base direct-workflow environment shared by the ant and humanoid locomotion tasks. @@ -29,9 +36,9 @@ class LocomotionDirectEnv(DirectRLEnv): workflows train against the same problem and converge to the same reward. """ - cfg: DirectRLEnvCfg + cfg: AntEnvCfg | HumanoidEnvCfg - def __init__(self, cfg: DirectRLEnvCfg, render_mode: str | None = None, **kwargs): + def __init__(self, cfg: AntEnvCfg | HumanoidEnvCfg, render_mode: str | None = None, **kwargs): super().__init__(cfg, render_mode, **kwargs) self.robot, self.terrain, self.joint_wrench = [ diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/__init__.py index f83c74bbc8e1..85a6de36400a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/__init__.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module contains the MDP functions shared by the locomotion (ant and humanoid) environments.""" +"""MDP terms shared by the locomotion (ant and humanoid) environments.""" from isaaclab.utils.module import lazy_export diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/__init__.pyi index 37304b499ea6..c54e760921a8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/__init__.pyi @@ -12,24 +12,16 @@ __all__ = [ "move_to_target_bonus", "power_consumption", "progress_reward", - "survival_success_rate", - "terminated_penalty", "upright_posture_bonus", ] -from isaaclab_tasks.core.locomotion.mdp.observations import ( - base_angle_to_target, - base_heading_proj, - base_up_proj, - base_yaw_roll, -) -from isaaclab_tasks.core.locomotion.mdp.rewards import ( +from .observations import base_angle_to_target, base_heading_proj, base_up_proj, base_yaw_roll +from .rewards import ( joint_pos_limits_penalty_ratio, move_to_target_bonus, power_consumption, progress_reward, - survival_success_rate, - terminated_penalty, upright_posture_bonus, ) + from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/observations.py index 38dbacc832a2..60580dd2636b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/observations.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Observation terms for the locomotion (ant and humanoid) environments.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/rewards.py index 2f9863019452..9818129b0d95 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/rewards.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Reward terms for the locomotion (ant and humanoid) environments.""" + from __future__ import annotations from typing import TYPE_CHECKING @@ -39,22 +41,10 @@ def move_to_target_bonus( return torch.where(heading_proj > threshold, 1.0, heading_proj / threshold) -def terminated_penalty(env: ManagerBasedRLEnv) -> torch.Tensor: - """One-off penalty for terminating early, independent of the environment step size. - - :class:`~isaaclab.managers.RewardManager` scales every term by the step interval, which would - make a plain terminal penalty depend on ``sim.dt`` and ``decimation``. Dividing by the step - interval here cancels that scaling, so the term contributes exactly its weight on the step the - episode terminates and stays equal to the death cost the direct workflow applies. - """ - return env.termination_manager.terminated.float() / env.step_dt - - class progress_reward(ManagerTermBase): """Reward for making progress towards the target.""" - def __init__(self, env: ManagerBasedRLEnv, cfg: RewardTermCfg): - # initialize the base class + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): super().__init__(cfg, env) # create history buffer self.potentials = torch.zeros(env.num_envs, device=env.device) @@ -89,38 +79,14 @@ def __call__( return self.potentials - self.prev_potentials -class survival_success_rate(ManagerTermBase): - """Tracks episode survival as the success metric. - - Returns zero reward (pure metric tracking). Flushes ``Metrics/success_rate`` - into ``extras["log"]`` on episode reset, where success = timed out without - early termination. - """ - - def reset(self, env_ids: torch.Tensor): - survived = self._env.termination_manager.time_outs[env_ids] - self._env.extras.setdefault("log", {})["Metrics/success_rate"] = survived.float().mean().item() - - def __call__(self, env: ManagerBasedRLEnv) -> torch.Tensor: - return torch.zeros(env.num_envs, device=env.device) - - class joint_pos_limits_penalty_ratio(ManagerTermBase): """Penalty for violating joint position limits weighted by the gear ratio.""" - def __init__(self, env: ManagerBasedRLEnv, cfg: RewardTermCfg): - # add default argument - asset_cfg = cfg.params.get("asset_cfg", SceneEntityCfg("robot")) - # extract the used quantities (to enable type-hinting) + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + asset_cfg: SceneEntityCfg = cfg.params.get("asset_cfg", SceneEntityCfg("robot")) asset: Articulation = env.scene[asset_cfg.name] - - # resolve the gear ratio for each joint - self.gear_ratio = torch.ones(env.num_envs, asset.num_joints, device=env.device) - index_list, _, value_list = string_utils.resolve_matching_names_values( - cfg.params["gear_ratio"], asset.joint_names - ) - self.gear_ratio[:, index_list] = torch.tensor(value_list, device=env.device) - self.gear_ratio_scaled = self.gear_ratio / torch.max(self.gear_ratio) + self.gear_ratio_scaled = _resolve_scaled_gear_ratio(cfg.params["gear_ratio"], asset, env.device) def __call__( self, @@ -150,19 +116,11 @@ class power_consumption(ManagerTermBase): This is computed as commanded torque times the joint velocity. """ - def __init__(self, env: ManagerBasedRLEnv, cfg: RewardTermCfg): - # add default argument - asset_cfg = cfg.params.get("asset_cfg", SceneEntityCfg("robot")) - # extract the used quantities (to enable type-hinting) + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + asset_cfg: SceneEntityCfg = cfg.params.get("asset_cfg", SceneEntityCfg("robot")) asset: Articulation = env.scene[asset_cfg.name] - - # resolve the gear ratio for each joint - self.gear_ratio = torch.ones(env.num_envs, asset.num_joints, device=env.device) - index_list, _, value_list = string_utils.resolve_matching_names_values( - cfg.params["gear_ratio"], asset.joint_names - ) - self.gear_ratio[:, index_list] = torch.tensor(value_list, device=env.device) - self.gear_ratio_scaled = self.gear_ratio / torch.max(self.gear_ratio) + self.gear_ratio_scaled = _resolve_scaled_gear_ratio(cfg.params["gear_ratio"], asset, env.device) def __call__( self, env: ManagerBasedRLEnv, gear_ratio: dict[str, float], asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") @@ -173,3 +131,22 @@ def __call__( return torch.sum( torch.abs(env.action_manager.action * asset.data.joint_vel.torch * self.gear_ratio_scaled), dim=-1 ) + + +def _resolve_scaled_gear_ratio(gear_ratio: dict[str, float], asset: Articulation, device: str) -> torch.Tensor: + """Resolve the per-joint gear ratios and normalize them by the largest one. + + Joints that the ``gear_ratio`` table does not match keep a unit gear. + + Args: + gear_ratio: Gear ratio per joint name expression. + asset: Articulation whose joint names the expressions are matched against. + device: Device of the returned tensor. + + Returns: + Gear ratios divided by the maximum gear ratio, shape ``(num_joints,)``. + """ + gears = torch.ones(asset.num_joints, device=device) + joint_ids, _, values = string_utils.resolve_matching_names_values(gear_ratio, asset.joint_names) + gears[joint_ids] = torch.tensor(values, device=device) + return gears / torch.max(gears) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/__init__.py index 9e2bba7a844b..e27dcc974a71 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/__init__.py @@ -23,9 +23,9 @@ disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}:PendulumMARLEnvCfg", - "default_agent": "skrl", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_marl_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_marl_ppo_cfg.yaml", + "default_agent": "skrl", "skrl_ippo_cfg_entry_point": f"{agents.__name__}:skrl_marl_ippo_cfg.yaml", "skrl_mappo_cfg_entry_point": f"{agents.__name__}:skrl_marl_mappo_cfg.yaml", }, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/__init__.py index 7a2415c2b864..ec9e27e89d53 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/__init__.py @@ -3,18 +3,16 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Franka reach environments.""" + import gymnasium as gym -from isaaclab_tasks.core.reach.config.franka import agents +from . import agents ## # Register Gym environments. ## -## -# Franka Reach -## - gym.register( id="Isaac-Reach-Franka", entry_point="isaaclab.envs:ManagerBasedRLEnv", @@ -28,10 +26,6 @@ }, ) -## -# Operational Space Control -## - gym.register( id="Isaac-Reach-Franka-OSC", entry_point="isaaclab.envs:ManagerBasedRLEnv", diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_env_cfg.py index 5874076d8612..484c5f1e8f57 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_env_cfg.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Franka Reach environment configuration.""" +"""Configuration for the Franka reach environment.""" import math @@ -26,11 +26,7 @@ from isaaclab_tasks.core.reach.reach_env_cfg import ReachEnvCfg from isaaclab_tasks.utils import PresetCfg, preset -## -# Pre-defined configs -## -from isaaclab_assets import FRANKA_PANDA_CFG, FRANKA_PANDA_MENAGERIE_CFG # isort: skip - +from isaaclab_assets import FRANKA_PANDA_CFG, FRANKA_PANDA_MENAGERIE_CFG ## # Environment configuration @@ -90,14 +86,12 @@ class FrankaReachEnvCfg(ReachEnvCfg): def validate_config(self) -> None: """Validate the selected controller and physics backend.""" - if isinstance(self.actions.arm_action, NewtonInverseKinematicsActionCfg) and not isinstance( self.sim.physics, NewtonCfg ): raise ValueError("The 'newton_ik' action preset requires a Newton physics preset.") - def __post_init__(self) -> None: - # post init of parent + def __post_init__(self): super().__post_init__() # Use the collision-complete legacy asset in PhysX until the Menagerie asset is corrected. diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_osc_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_osc_env_cfg.py index c6f4ae78e539..19c2f194e70d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_osc_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_osc_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the Franka reach environment with operational space control.""" + import warnings from isaaclab_newton.sim.schemas import MujocoRigidBodyCfg @@ -17,12 +19,10 @@ from isaaclab_tasks.utils import preset -class _DeprecatedDiffIKAbsWeight(float): - """Marker for the deprecated ``diffik_abs`` no-op alias; replaced by a plain float during validation.""" - - @configclass class FrankaReachEnvCfg(franka_reach_env_cfg.FrankaReachEnvCfg): + """Franka reach configuration driven by an operational space controller.""" + def validate_config(self) -> None: """Validate the physics backend and warn about the deprecated ``diffik_abs`` alias.""" super().validate_config() @@ -37,8 +37,7 @@ def validate_config(self) -> None: ) self.rewards.action_magnitude.weight = float(weight) - def __post_init__(self) -> None: - # post init of parent + def __post_init__(self): super().__post_init__() # Use an explicit actuator to enforce the USD-authored effort limits for effort control. Keep the @@ -65,16 +64,12 @@ def __post_init__(self) -> None: default=default_weight, diffik_abs=_DeprecatedDiffIKAbsWeight(default_weight) ) - # If closed-loop contact force control is desired, contact sensors should be enabled for the robot - # self.scene.robot.spawn.activate_contact_sensors = True - + # closed-loop contact force control would additionally need contact sensors on the robot self.actions.arm_action = OperationalSpaceControllerActionCfg( asset_name="robot", joint_names=["panda_joint.*"], body_name="panda_hand", - # If a task frame different from articulation root/base is desired, a RigidObject, e.g., "task_frame", - # can be added to the scene and its relative path could provided as task_frame_rel_path - # task_frame_rel_path="task_frame", + # a task frame other than the articulation root can be provided through ``task_frame_rel_path`` controller_cfg=OperationalSpaceControllerCfg( target_types=["pose_abs"], impedance_mode="variable_kp", @@ -96,8 +91,10 @@ def __post_init__(self) -> None: self.observations.policy.joint_vel = None def play_mode(self): - # play-mode overrides of parent super().play_mode() - # make a smaller scene for play self.scene.num_envs = 16 + + +class _DeprecatedDiffIKAbsWeight(float): + """Marker for the deprecated ``diffik_abs`` no-op alias; replaced by a plain float during validation.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/ur_10/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/ur_10/__init__.py index 8424eb7fc250..1484366b5a17 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/ur_10/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/ur_10/__init__.py @@ -3,9 +3,11 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""UR10 reach environment.""" + import gymnasium as gym -from isaaclab_tasks.core.reach.config.ur_10 import agents +from . import agents ## # Register Gym environments. diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/ur_10/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/ur_10/joint_pos_env_cfg.py index 4adfcf120d67..4a9af7b83e39 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/ur_10/joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/ur_10/joint_pos_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the UR10 reach environment.""" + import math import isaaclab.envs.mdp as mdp @@ -10,11 +12,7 @@ from isaaclab_tasks.core.reach.reach_env_cfg import ReachEnvCfg -## -# Pre-defined configs -## -from isaaclab_assets import UR10_CFG # isort: skip - +from isaaclab_assets import UR10_CFG ## # Environment configuration @@ -23,8 +21,9 @@ @configclass class UR10ReachEnvCfg(ReachEnvCfg): - def __post_init__(self) -> None: - # post init of parent + """UR10 reach configuration with joint position control.""" + + def __post_init__(self): super().__post_init__() # switch robot to ur10 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reach/reach_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reach/reach_env_cfg.py index 033cb642ee1d..e7cd5d992526 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reach/reach_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reach/reach_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Base configuration for the end-effector reach environments.""" + from dataclasses import MISSING from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg @@ -36,6 +38,8 @@ @configclass class ReachPhysicsCfg(PresetCfg): + """Physics backend presets for the reach environments.""" + isaacsim_physx: PhysxCfg = PhysxCfg(bounce_threshold_velocity=0.2) ovphysx: OvPhysxCfg = OvPhysxCfg() physx: PhysxAutoCfg = PhysxAutoCfg(isaacsim_physx=isaacsim_physx, ovphysx=ovphysx) @@ -140,7 +144,7 @@ class PolicyCfg(ObsGroup): pose_command = ObsTerm(func=mdp.generated_commands, params={"command_name": "ee_pose"}) actions = ObsTerm(func=mdp.last_action) - def __post_init__(self) -> None: + def __post_init__(self): self.enable_corruption = True self.concatenate_terms = True @@ -234,13 +238,14 @@ class ReachEnvCfg(ManagerBasedRLEnvCfg): events: EventCfg = EventCfg() curriculum: CurriculumCfg = CurriculumCfg() - def __post_init__(self) -> None: + def __post_init__(self): """Post initialization.""" # general settings self.decimation = 4 - self.sim.render_interval = self.decimation self.episode_length_s = 12.0 - self.sim.default_visualizer_cfg = VisualizerCfg(eye=(3.5, 3.5, 3.5)) # simulation settings - self.sim.dt = 1.0 / 120.0 + self.sim.dt = 1 / 120 + self.sim.render_interval = self.decimation self.sim.physics = ReachPhysicsCfg() + # visualizer settings + self.sim.default_visualizer_cfg = VisualizerCfg(eye=(3.5, 3.5, 3.5)) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/__init__.py index b12e5252b837..1839b8c0a00c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/__init__.py @@ -17,5 +17,4 @@ modifications and additional features. .. _dexterous cube manipulation: https://github.com/NVIDIA-Omniverse/IsaacGymEnvs/blob/main/isaacgymenvs/tasks/allegro_hand.py - """ diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/__init__.py index 91581eec335d..1b2f8756dfbf 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/__init__.py @@ -7,7 +7,7 @@ import gymnasium as gym -from isaaclab_tasks.core.reorient.config.allegro_hand import agents +from . import agents ## # Register Gym environments -- direct workflow. @@ -36,9 +36,9 @@ disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.allegro_hand_manager_env_cfg:AllegroHandManagerEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AllegroHandManagerPPORunnerCfg", "default_agent": "rsl_rl", - "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", }, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py index 7ff631ad28a8..81d0365ec3c6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py @@ -53,9 +53,22 @@ ) """In-hand cube for the Allegro reorientation task.""" +GOAL_OBJECT_CFG = VisualizationMarkersCfg( + prim_path="/Visuals/goal_marker", + markers={ + "goal": sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", + scale=(1.2, 1.2, 1.2), + ) + }, +) +"""Goal cube marker for the reorientation environments.""" + @configclass class PhysicsCfg(PresetCfg): + """Physics backend presets for the Allegro Hand reorientation environments.""" + isaacsim_physx = PhysxCfg( bounce_threshold_velocity=0.2, ) @@ -73,14 +86,3 @@ class PhysicsCfg(PresetCfg): ovphysx = OvPhysxCfg() physx = PhysxAutoCfg(isaacsim_physx=isaacsim_physx, ovphysx=ovphysx) default = newton_mjwarp - - -GOAL_OBJECT_CFG = VisualizationMarkersCfg( - prim_path="/Visuals/goal_marker", - markers={ - "goal": sim_utils.UsdFileCfg( - usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", - scale=(1.2, 1.2, 1.2), - ) - }, -) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_direct_env_cfg.py index 636bc639654d..02b7543eb89a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_direct_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the direct-workflow Allegro Hand reorientation environment.""" + import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg from isaaclab.envs import DirectRLEnvCfg @@ -39,6 +41,8 @@ class AllegroHandSceneCfg(InteractiveSceneCfg): @configclass class AllegroHandEnvCfg(DirectRLEnvCfg): + """Configuration for the direct-workflow Allegro Hand cube reorientation environment.""" + # env decimation = 4 episode_length_s = 10.0 @@ -48,7 +52,7 @@ class AllegroHandEnvCfg(DirectRLEnvCfg): asymmetric_obs = False obs_type = "full" - # simulation — values mirrored by the manager cfg + # simulation, mirrored by the manager-based configuration sim: SimulationCfg = SimulationCfg( dt=1 / 120, render_interval=decimation, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py index 52a85582adbc..1314f7b360df 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py @@ -139,4 +139,5 @@ class AllegroHandManagerEnvCfg(ReorientManagerEnvBaseCfg): def __post_init__(self): super().__post_init__() + # simulation settings self.sim.physics = PhysicsCfg() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/__init__.py index 09f51a4cd7e8..eda87abce1ec 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/__init__.py @@ -3,16 +3,14 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Shadow Hand environment. -""" +"""Shadow Hand in-hand cube reorientation environments (direct and manager-based workflows).""" import gymnasium as gym -from isaaclab_tasks.core.reorient.config.shadow_hand import agents +from . import agents ## -# Register Gym environments. +# Register Gym environments -- state-based. ## gym.register( @@ -27,6 +25,7 @@ "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", }, ) + gym.register( id="Isaac-Reorient-Cube-Shadow", entry_point="isaaclab.envs:ManagerBasedRLEnv", @@ -40,9 +39,9 @@ }, ) -# ------- -# Vision -# ------- +## +# Register Gym environments -- camera-based. +## gym.register( id="Isaac-Reorient-Cube-Shadow-Camera-Direct", @@ -50,19 +49,20 @@ disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.shadow_hand_direct_camera_env_cfg:ShadowHandCameraEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_camera_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandCameraFFPPORunnerCfg", "default_agent": "rsl_rl", - "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_camera_cfg.yaml", }, ) + gym.register( id="Isaac-Reorient-Cube-Shadow-Camera", entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.shadow_hand_camera_manager_env_cfg:ShadowHandCameraManagerEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_camera_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandCameraFFPPORunnerCfg", "default_agent": "rsl_rl", - "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_camera_cfg.yaml", }, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/feature_extractor.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/feature_extractor.py index 4cec0b16ab4f..afed1f928cb4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/feature_extractor.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/feature_extractor.py @@ -3,7 +3,12 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""CNN feature extractor regressing cube keypoints from the Shadow Hand tiled camera.""" + +from __future__ import annotations + import glob +import logging import os import torch @@ -14,6 +19,8 @@ from isaaclab.utils import configclass from isaaclab.utils.assets import retrieve_file_path +logger = logging.getLogger(__name__) + # Number of output channels for each supported camera data type. _DATA_TYPE_CHANNELS: dict[str, int] = { "rgb": 3, @@ -38,11 +45,6 @@ ) -def _conv_out(size: int, kernel: int, stride: int, padding: int = 0) -> int: - """Compute the spatial output size of a single convolutional layer.""" - return (size + 2 * padding - kernel) // stride + 1 - - class FeatureExtractorNetwork(nn.Module): """CNN architecture used to regress keypoint positions of the in-hand cube from image data.""" @@ -110,7 +112,7 @@ def __init__( self._imagenet_norm_ranges.append((channel_idx, channel_idx + n_ch)) channel_idx += n_ch - def forward(self, x): + def forward(self, x: torch.Tensor) -> torch.Tensor: x = x.permute(0, 3, 1, 2).clone() for start, end in self._imagenet_norm_ranges: x[:, start:end, :, :] = self.data_transforms(x[:, start:end, :, :]) @@ -129,14 +131,6 @@ class FeatureExtractorCfg: load_checkpoint: bool = False """If True, the feature extractor model is loaded from a checkpoint. Default is False.""" - pretrained_checkpoint: str | None = None - """Fallback feature-extractor checkpoint to load when no local checkpoint exists. - - This may be a local or remote path. :class:`FeatureExtractor` first looks for the latest - local ``*.pth`` checkpoint in the log directory, then retrieves this checkpoint when configured. - Default is None. - """ - write_image_to_file: bool = False """If True, the images from the camera sensor are written to file. Default is False.""" @@ -147,6 +141,14 @@ class FeatureExtractorCfg: for benchmarking rendering throughput without CNN inference overhead. Default is True. """ + pretrained_checkpoint: str | None = None + """Fallback feature-extractor checkpoint to load when no local checkpoint exists. + + This may be a local or remote path. :class:`FeatureExtractor` first looks for the latest + local ``*.pth`` checkpoint in the log directory, then retrieves this checkpoint when configured. + Default is None. + """ + class FeatureExtractor: """Class for extracting features from image data. @@ -210,7 +212,7 @@ def __init__( if self.cfg.load_checkpoint: checkpoint = self._resolve_checkpoint_path() - print(f"[INFO]: Loading feature extractor checkpoint from {checkpoint}") + logger.info("Loading feature extractor checkpoint from %s", checkpoint) self.feature_extractor.load_state_dict(torch.load(checkpoint, weights_only=True)) if self.cfg.train: @@ -223,8 +225,8 @@ def __init__( def _preprocess_images(self, camera_output: dict[str, torch.Tensor]) -> torch.Tensor: """Preprocesses and concatenates camera images into a single tensor. - Each data type in :attr:`FeatureExtractorCfg.data_types` is extracted from - ``camera_output``, normalized, and concatenated along the channel dimension. + Each data type in :attr:`data_types` is extracted from ``camera_output``, normalized, and + concatenated along the channel dimension. Args: camera_output: Dictionary mapping data type names to image tensors. @@ -337,6 +339,11 @@ def _resolve_checkpoint_path(self) -> str: if local_checkpoints: return max(local_checkpoints, key=os.path.getctime) if self.cfg.pretrained_checkpoint is not None: - print(f"[INFO]: Fetching pretrained feature extractor checkpoint from {self.cfg.pretrained_checkpoint}") + logger.info("Fetching pretrained feature extractor checkpoint from %s", self.cfg.pretrained_checkpoint) return retrieve_file_path(self.cfg.pretrained_checkpoint) raise FileNotFoundError(f"No feature-extractor checkpoint found in '{self.log_dir}'.") + + +def _conv_out(size: int, kernel: int, stride: int, padding: int = 0) -> int: + """Compute the spatial output size of a single convolutional layer.""" + return (size + 2 * padding - kernel) // stride + 1 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py index cb9916ee4f7f..e19235951879 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py @@ -18,11 +18,10 @@ validate_shadow_hand_camera_settings, ) from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_manager_env_cfg import ( - ReorientFullStateObsCfg, ShadowHandManagerEnvCfg, ShadowHandManagerSceneCfg, ) -from isaaclab_tasks.core.reorient.reorient_manager_env_cfg import ReorientRobotObsCfg +from isaaclab_tasks.core.reorient.reorient_manager_env_cfg import ReorientFullStateObsCfg, ReorientRobotObsCfg from isaaclab_tasks.utils import preset from isaaclab_assets.robots.shadow_hand import FINGERTIP_NAMES @@ -43,7 +42,7 @@ class ShadowHandCameraManagerSceneCfg(ShadowHandManagerSceneCfg): num_envs = 1225 env_spacing = 2.0 - # does it not need ground? or is ground needed at all in general? + # no ground plane, matching the Direct camera scene ground = None tiled_camera: ShadowHandTiledCameraCfg = ShadowHandTiledCameraCfg() joint_wrench = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot") @@ -62,10 +61,9 @@ class CameraPolicyCfg(ReorientRobotObsCfg): """ goal_pose = ObsTerm(func=mdp.generated_commands, params={"command_name": "object_pose"}) - # No action_name, deliberately: omitting it returns the WHOLE action vector, whereas - # naming a term returns only that term's raw actions. This hand's twenty motors are - # split across a joint term and a tendon term, so naming either would feed the policy - # half of its own last action. + # no action_name: omitting it returns the whole action vector, whereas naming a term returns + # only that term's raw actions. This hand's twenty motors are split across a joint term and a + # tendon term, so naming either would feed the policy half of its own last action. last_action = ObsTerm(func=mdp.last_action) camera_features = ObsTerm( func=mdp.ShadowHandCameraFeatures, @@ -116,8 +114,7 @@ class ShadowHandCameraManagerEnvCfg(ShadowHandManagerEnvCfg): def __post_init__(self): super().__post_init__() - # camera tasks display the goal inside the tiled camera's frustum - # goal cube must sit inside the tiled camera's frustum + # the goal cube must sit inside the tiled camera's frustum self.commands.object_pose.fixed_marker_pos = (-0.2, 0.1, 0.6) self.observations.policy.camera_features.params["feature_extractor_cfg"] = self.feature_extractor diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py index 39313db64de7..6241fdd9a950 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py @@ -15,7 +15,6 @@ from isaaclab_physx.physics import PhysxCfg from isaaclab_physx.sim.schemas import PhysxRigidBodyCfg -import isaaclab.envs.mdp as mdp import isaaclab.sim as sim_utils from isaaclab.assets import RigidObjectCfg from isaaclab.managers import EventTermCfg as EventTerm @@ -25,13 +24,10 @@ from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR -import isaaclab_tasks.core.reorient.mdp as reorient_mdp +import isaaclab_tasks.core.reorient.mdp as mdp from isaaclab_tasks.utils import PresetCfg -from isaaclab_assets.robots.shadow_hand import ( - SHADOW_HAND_NEWTON_CFG, - SHADOW_HAND_PHYSX_CFG, -) +from isaaclab_assets.robots.shadow_hand import SHADOW_HAND_NEWTON_CFG, SHADOW_HAND_PHYSX_CFG @configclass @@ -131,7 +127,7 @@ class ShadowHandManagerResetEventCfg: }, ) reset_hand = EventTerm( - func=reorient_mdp.reset_reorient_hand, + func=mdp.reset_reorient_hand, mode="reset", params={ "joint_position_noise": 0.2, # [rad] @@ -157,12 +153,12 @@ class ShadowHandManagerEventPresetCfg(PresetCfg): class ShadowHandRobotCfg(PresetCfg): """The same hand on every engine; only the asset's physics variant differs. - The variant is a property of the asset -- its MuJoCo and PhysX payloads are mutually exclusive - -- so selecting it per engine is unavoidable. Nothing else may differ here: a spawn pose or gain - that needs a per-engine value is a defect to fix in the asset, not a preset to add. + The variant is a property of the asset, whose MuJoCo and PhysX payloads are mutually exclusive, so + selecting it per engine is unavoidable. Nothing else may differ here: a spawn pose or gain that + needs a per-engine value is a defect to fix in the asset, not a preset to add. """ - # `spawn_path` authors only the prototype env; the scene clone plan replicates the rest (#7036). + # ``spawn_path`` authors only the prototype env; the scene clone plan replicates the rest newton_mjwarp = SHADOW_HAND_NEWTON_CFG.replace( prim_path="{ENV_REGEX_NS}/Robot", spawn=SHADOW_HAND_NEWTON_CFG.spawn.replace(spawn_path="/World/envs/env_0/Robot"), @@ -199,19 +195,27 @@ class ShadowHandRobotCfg(PresetCfg): ], semantic_tags=[("class", "cube")], ), - # Above the palm of the hand, which lies horizontal reaching along -Y. This is the position the - # previous Newton asset used, and it applies unchanged because the two assets are the same hand - # in the same frame (Kabsch residual 0.00 mm between their USDs). - # - # Values tuned against a vertically-standing hand do NOT belong here: that pose was itself a - # mistake, and a cube placed for it sits 0.37 m from this palm. + # above the palm of the hand, which lies horizontal reaching along -Y; both engine assets share + # this frame, so the position applies to either init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.39, 0.6), rot=(0.0, 0.0, 0.0, 1.0)), ) """In-hand cube for the Shadow Hand reorientation tasks.""" +GOAL_OBJECT_CFG = VisualizationMarkersCfg( + prim_path="/Visuals/goal_marker", + markers={ + "goal": sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", + ) + }, +) +"""Goal cube marker for the reorientation environments.""" + @configclass class PhysicsCfg(PresetCfg): + """Physics backend presets for the Shadow Hand reorientation environments.""" + isaacsim_physx = PhysxCfg( bounce_threshold_velocity=0.2, gpu_max_rigid_contact_count=2**23, @@ -231,13 +235,3 @@ class PhysicsCfg(PresetCfg): ovphysx = OvPhysxCfg() physx = PhysxAutoCfg(isaacsim_physx=isaacsim_physx, ovphysx=ovphysx) default = newton_mjwarp - - -GOAL_OBJECT_CFG = VisualizationMarkersCfg( - prim_path="/Visuals/goal_marker", - markers={ - "goal": sim_utils.UsdFileCfg( - usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", - ) - }, -) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py index ef9c5297c5ae..9bd29f3de7c5 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Direct-workflow Shadow Hand reorientation environment with camera observations.""" from __future__ import annotations @@ -21,14 +22,15 @@ class ShadowHandCameraEnv(ShadowHandDirectEnv): + """Shadow Hand reorientation whose policy sees CNN embeddings of a tiled camera instead of the object state.""" + cfg: ShadowHandCameraEnvCfg def __init__(self, cfg: ShadowHandCameraEnvCfg, render_mode: str | None = None, **kwargs): super().__init__(cfg, render_mode, **kwargs) self._tiled_camera = self.scene["tiled_camera"] - # Derive CNN input data types from the resolved camera config so that any camera - # preset (e.g. presets=rgb, presets=albedo) automatically configures the right - # network input channels without requiring a separate env config class. + # the CNN input channels follow the resolved camera data types, so any camera preset + # (e.g. presets=rgb) configures the network without a separate environment config self.feature_extractor = FeatureExtractor( self.cfg.feature_extractor, self.device, @@ -43,7 +45,7 @@ def __init__(self, cfg: ShadowHandCameraEnvCfg, render_mode: str | None = None, self.gt_keypoints = torch.ones(self.num_envs, 8, 3, dtype=torch.float32, device=self.device) self.goal_keypoints = torch.ones(self.num_envs, 8, 3, dtype=torch.float32, device=self.device) - def _compute_image_observations(self): + def _compute_image_observations(self) -> torch.Tensor: # generate ground truth keypoints for in-hand cube compute_cube_keypoints(pose=torch.cat((self.object_pos, self.object_rot), dim=1), out=self.gt_keypoints) @@ -77,9 +79,9 @@ def _compute_image_observations(self): return obs - def _compute_proprio_observations(self): + def _compute_proprio_observations(self) -> torch.Tensor: """Proprioception observations from physics.""" - obs = torch.cat( + return torch.cat( ( # hand scale_transform(self.hand_dof_pos, self.hand_dof_lower_limits, self.hand_dof_upper_limits), @@ -96,22 +98,16 @@ def _compute_proprio_observations(self): ), dim=-1, ) - return obs - def _compute_states(self): + def _compute_states(self) -> torch.Tensor: """Asymmetric states for the critic.""" - sim_states = self.compute_full_state() - state = torch.cat((sim_states, self.embeddings), dim=-1) - return state + return torch.cat((self.compute_full_state(), self.embeddings), dim=-1) def _get_observations(self) -> dict: # proprioception observations state_obs = self._compute_proprio_observations() - # vision observations from CMM + # vision observations from the CNN image_obs = self._compute_image_observations() obs = torch.cat((state_obs, image_obs), dim=-1) self._update_fingertip_force_sensors() - state = self._compute_states() - - observations = {"policy": obs, "critic": state} - return observations + return {"policy": obs, "critic": self._compute_states()} diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py index 66dba264ad52..9cd43663e64a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the direct-workflow Shadow Hand camera reorientation environment.""" + from __future__ import annotations import isaaclab.sim as sim_utils @@ -184,6 +186,8 @@ class ShadowHandCameraSceneCfg(ShadowHandSceneCfg): @configclass class ShadowHandCameraEnvCfg(ShadowHandEnvCfg): + """Configuration for the direct-workflow Shadow Hand camera reorientation environment.""" + # scene scene: ShadowHandCameraSceneCfg = ShadowHandCameraSceneCfg() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env_cfg.py index ffc5191dcb13..76cb3bee5ba8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the direct-workflow Shadow Hand reorientation environment.""" + import isaaclab.sim as sim_utils from isaaclab.assets import AssetBaseCfg, RigidObjectCfg from isaaclab.envs import DirectRLEnvCfg @@ -20,12 +22,7 @@ ShadowHandRobotCfg, ) -from isaaclab_assets.robots.shadow_hand import ( - FINGERTIP_NAMES, - JOINT_NAMES, - TENDON_NAMES, - TENDON_POSITION_LIMITS, -) +from isaaclab_assets.robots.shadow_hand import FINGERTIP_NAMES, JOINT_NAMES, TENDON_NAMES, TENDON_POSITION_LIMITS @configclass @@ -47,6 +44,8 @@ class ShadowHandSceneCfg(InteractiveSceneCfg): @configclass class ShadowHandEnvCfg(DirectRLEnvCfg): + """Configuration for the direct-workflow Shadow Hand cube reorientation environment.""" + # env decimation = 2 episode_length_s = 10.0 @@ -56,7 +55,7 @@ class ShadowHandEnvCfg(DirectRLEnvCfg): asymmetric_obs = False obs_type = "full" - # simulation — values mirrored by the manager cfg + # simulation, mirrored by the manager-based configuration sim: SimulationCfg = SimulationCfg( dt=1 / 120, render_interval=decimation, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py index f7dbf5adbff6..d1e03e163222 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py @@ -28,15 +28,10 @@ ) from isaaclab_tasks.utils import PresetCfg -from isaaclab_assets.robots.shadow_hand import ( - FINGERTIP_NAMES, - JOINT_NAMES, - TENDON_NAMES, - TENDON_POSITION_LIMITS, -) +from isaaclab_assets.robots.shadow_hand import FINGERTIP_NAMES, JOINT_NAMES, TENDON_NAMES, TENDON_POSITION_LIMITS ## -# Default: full-state actor. +# Full-state actor (default) ## @@ -85,11 +80,12 @@ class ShadowHandManagerEnvCfg(ReorientManagerEnvBaseCfg): def __post_init__(self): super().__post_init__() + # simulation settings self.sim.physics = PhysicsCfg() ## -# ``presets=asymmetric``: reduced actor, privileged critic. +# Reduced actor with privileged critic (``presets=asymmetric``) ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.py index a69b931a8cfb..e1c3d8dfb9dc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module contains the functions that are specific to the in-hand manipulation environments.""" +"""MDP terms specific to the in-hand reorientation environments.""" from isaaclab.utils.module import lazy_export diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py index d1980d5c2d9c..ee9f8609a319 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py @@ -65,9 +65,7 @@ def __init__(self, cfg: ReorientCommandCfg, env: ManagerBasedRLEnv): self.pos_command_w = self.pos_command_e + self._env.scene.env_origins # -- orientation: (x, y, z, w) self.quat_command_w = torch.zeros(self.num_envs, 4, device=self.device) - self.quat_command_w[:, 3] = 1.0 # set the scalar component to 1.0 - - # -- unit vectors + self.quat_command_w[:, 3] = 1.0 # identity quaternion in (x, y, z, w) layout # -- metrics self.metrics["orientation_error"] = torch.zeros(self.num_envs, device=self.device) @@ -75,6 +73,8 @@ def __init__(self, cfg: ReorientCommandCfg, env: ManagerBasedRLEnv): self.metrics["consecutive_success"] = torch.zeros(self.num_envs, device=self.device) self.metrics["success_rate"] = torch.zeros(self.num_envs, device=self.device) self._success = SuccessTracker(self.num_envs, self.device) + # marker positions are constant per run and cached on first use to avoid a per-frame host-to-device copy + self._marker_pos_offset: torch.Tensor | None = None self._fixed_marker_pos_w: torch.Tensor | None = None # adds (optional) cmd kind and element names for leapp export @@ -134,8 +134,7 @@ def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: def _resample_command(self, env_ids: Sequence[int]): self._success.record_goal_reached(env_ids) - # The shared sampler covers SO(3) uniformly. Composing a rotation about x with one about y, as - # this did, reaches only a two-axis subset and needs a unit-axis buffer per axis to do it. + # sample uniformly over SO(3) rather than composing single-axis rotations, which only reaches a subset quat = math_utils.random_orientation(len(env_ids), device=self.device) # make sure the quaternion real-part is always positive self.quat_command_w[env_ids] = math_utils.quat_unique(quat) if self.cfg.make_quat_unique else quat @@ -162,13 +161,13 @@ def _set_debug_vis_impl(self, debug_vis: bool): def _debug_vis_callback(self, event): if self.cfg.fixed_marker_pos is None: - marker_pos = self.pos_command_w + torch.tensor(self.cfg.marker_pos_offset, device=self.device) + if self._marker_pos_offset is None: + self._marker_pos_offset = torch.tensor(self.cfg.marker_pos_offset, device=self.device) + marker_pos = self.pos_command_w + self._marker_pos_offset else: if self._fixed_marker_pos_w is None: - # constant per run; cached to avoid a host-to-device allocation every render frame self._fixed_marker_pos_w = ( - torch.tensor(self.cfg.fixed_marker_pos, device=self.device).repeat(self.num_envs, 1) - + self._env.scene.env_origins + torch.tensor(self.cfg.fixed_marker_pos, device=self.device) + self._env.scene.env_origins ) marker_pos = self._fixed_marker_pos_w self.goal_pose_visualizer.visualize( diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/events.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/events.py index c3d7ffe26b8c..325268500af2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/events.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Reset events for state-based in-hand reorientation tasks.""" +"""Event terms for the in-hand reorientation environments.""" from __future__ import annotations diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py index d1806712b18d..a1b1ac7958e7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py @@ -3,10 +3,11 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Functions specific to the in-hand dexterous manipulation environments.""" +"""Observation terms for the in-hand reorientation environments.""" from __future__ import annotations +import functools from typing import TYPE_CHECKING import torch @@ -17,7 +18,7 @@ if TYPE_CHECKING: from collections.abc import Sequence - from isaaclab.assets import RigidObject + from isaaclab.assets import Articulation, RigidObject from isaaclab.envs import ManagerBasedRLEnv from isaaclab.sensors import Camera @@ -30,20 +31,6 @@ """Half side lengths [m] of the reorientation cube.""" -# -- cube keypoint helpers, shared by the camera and state observation terms -def _cube_corner_offsets( - size: tuple[float, float, float], num_keypoints: int, device: torch.device | str -) -> torch.Tensor: - """Corner offsets [m] from the cube center; corner index bits select the +/- half side per axis.""" - signs = torch.tensor( - [[1 - 2 * ((corner >> axis) & 1) for axis in range(3)] for corner in range(num_keypoints)], - dtype=torch.float32, - device=device, - ) - half_size = torch.tensor(size, dtype=torch.float32, device=device) / 2.0 - return signs * half_size - - def compute_cube_keypoints( pose: torch.Tensor, num_keypoints: int = 8, @@ -61,17 +48,12 @@ def compute_cube_keypoints( Returns: Cube-corner positions [m], shape ``(num_envs, num_keypoints, 3)``. """ - # Vectorized over corners: the earlier implementation looped over the eight corners, - # allocating a tensor and calling quat_apply once per corner. The corner sign-offsets - # are pose-independent, so they are built once and all num_keypoints corners are rotated - # by the pose in a single batched quat_apply — mathematically identical, no Python loop. num_envs = pose.shape[0] - corners = _cube_corner_offsets(size, num_keypoints, pose.device) - # Broadcast each env's quaternion across its corners and rotate every offset at once. + corners = _cube_corner_offsets(size, num_keypoints, str(pose.device)) + # rotate every corner offset by its environment's quaternion in one batched call rotated = math_utils.quat_apply( pose[:, None, 3:7].expand(num_envs, num_keypoints, 4), corners.expand(num_envs, num_keypoints, 3) ) - # Translate the rotated offsets by the cube-center position to get world-frame corners. keypoints = pose[:, None, 0:3] + rotated if out is None: return keypoints @@ -96,14 +78,13 @@ def cube_keypoints_from_quat( """ num_envs = quat.shape[0] size = (2.0 * half_size[0], 2.0 * half_size[1], 2.0 * half_size[2]) - corners = _cube_corner_offsets(size, num_keypoints, quat.device) + corners = _cube_corner_offsets(size, num_keypoints, str(quat.device)) rotated = math_utils.quat_apply( quat[:, None, :].expand(num_envs, num_keypoints, 4), corners.expand(num_envs, num_keypoints, 3) ) return rotated.reshape(num_envs, num_keypoints * 3) -# -- command terms def goal_quat_diff( env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg, command_name: str, make_quat_unique: bool ) -> torch.Tensor: @@ -128,18 +109,16 @@ def goal_quat_diff( return math_utils.quat_unique(quat_error) if make_quat_unique else quat_error -# -- fingertip terms -# Task-local because the framework provides body_pose_w but no body_pos_w or body_vel_w. def fingertip_pos(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: """Flattened fingertip positions in the environment frame [m], shape ``(num_envs, num_fingertips * 3)``.""" - asset = env.scene[asset_cfg.name] + asset: Articulation = env.scene[asset_cfg.name] positions = asset.data.body_pos_w.torch[:, asset_cfg.body_ids] - env.scene.env_origins.unsqueeze(1) return positions.reshape(env.num_envs, -1) def fingertip_vel(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: """Flattened fingertip spatial velocities [m/s, rad/s], shape ``(num_envs, num_fingertips * 6)``.""" - asset = env.scene[asset_cfg.name] + asset: Articulation = env.scene[asset_cfg.name] return asset.data.body_vel_w.torch[:, asset_cfg.body_ids].reshape(env.num_envs, -1) @@ -153,7 +132,7 @@ def shadow_hand_goal_keypoints(env: ManagerBasedRLEnv, command_name: str) -> tor Returns: Flattened zero-origin cube keypoints [m], shape ``(num_envs, 24)``. """ - command_term = env.command_manager.get_term(command_name) + command_term: ReorientCommand = env.command_manager.get_term(command_name) return cube_keypoints_from_quat(command_term.quat_command_w) @@ -179,9 +158,8 @@ def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): super().__init__(cfg, env) sensor_cfg: SceneEntityCfg = cfg.params["sensor_cfg"] camera: Camera = env.scene.sensors[sensor_cfg.name] - # Runtime-only import: the mdp layer must not import the task-config layer - # at module load (config modules import mdp; see the layering note above). - from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractor + # deferred import: the config modules import this mdp package at module load + from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractor # noqa: PLC0415 feature_extractor_cfg: FeatureExtractorCfg = cfg.params["feature_extractor_cfg"] self._feature_extractor = FeatureExtractor( @@ -249,3 +227,18 @@ def __call__( if pose_loss is not None: env.extras.setdefault("log", {})["pose_loss"] = pose_loss return embeddings + + +@functools.cache +def _cube_corner_offsets(size: tuple[float, float, float], num_keypoints: int, device: str) -> torch.Tensor: + """Corner offsets [m] from the cube center; corner index bits select the +/- half side per axis. + + Cached per ``(size, num_keypoints, device)`` so the hot path does not rebuild the constant every step. + """ + signs = torch.tensor( + [[1 - 2 * ((corner >> axis) & 1) for axis in range(3)] for corner in range(num_keypoints)], + dtype=torch.float32, + device=device, + ) + half_size = torch.tensor(size, dtype=torch.float32, device=device) / 2.0 + return signs * half_size diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/rewards.py index 9974462578f4..05f244726fd1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/rewards.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Functions specific to the in-hand dexterous manipulation environments.""" +"""Reward terms for the in-hand reorientation environments.""" from __future__ import annotations @@ -57,7 +57,7 @@ def track_pos_l2( Args: env: The environment object. - command_term: The command term to be used for extracting the goal. + command_name: The command term to be used for extracting the goal. object_cfg: The configuration for the scene entity. Default is "object". """ # extract useful elements diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/terminations.py index 2c4579170ef5..f3eec876a1e9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/terminations.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Functions specific to the in-hand dexterous manipulation environments.""" +"""Termination terms for the in-hand reorientation environments.""" from __future__ import annotations diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py index a421796bc8b1..7a1dd5167f77 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Direct-workflow in-hand reorientation environment shared by the Allegro and Shadow hands.""" from __future__ import annotations @@ -33,79 +34,9 @@ from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_env_cfg import ShadowHandEnvCfg -@torch.jit.script -def reorient_reward( - reset_buf: torch.Tensor, - reset_goal_buf: torch.Tensor, - successes: torch.Tensor, - consecutive_successes: torch.Tensor, - object_pos: torch.Tensor, - target_pos: torch.Tensor, - goal_reached: torch.Tensor, - rotation_distance: torch.Tensor, - actions: torch.Tensor, - distance_scale: float, - rotation_scale: float, - rotation_epsilon: float, - action_penalty_scale: float, - success_bonus: float, - fall_distance: float, - fall_penalty: float, - averaging_factor: float, -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: - """Compute the Direct reorientation reward and success state transition. - - The success evaluation is not recomputed here: callers pass the flags and - orientation errors computed once - per step. - - Args: - reset_buf: Current episode-reset flags. - reset_goal_buf: Current goal-reset flags. - successes: Goals reached in each episode. - consecutive_successes: Moving-average success count. - object_pos: Object positions in the environment frame [m]. - target_pos: Goal positions in the environment frame [m]. - goal_reached: Per-environment success flags for this step. - rotation_distance: Per-environment orientation errors [rad]. - actions: Normalized joint actions. - distance_scale: Position-distance reward scale [1/m]. - rotation_scale: Orientation reward scale [rad]. - rotation_epsilon: Orientation reward regularizer [rad]. - action_penalty_scale: Squared-action reward scale. - success_bonus: Reward added when a goal is reached. - fall_distance: Object-to-goal termination distance [m]. - fall_penalty: Reward added when the object is out of reach. - averaging_factor: Consecutive-success moving-average factor. - - Returns: - Reward, goal-reset flags, episode success counts, and moving-average - consecutive successes. - """ - goal_distance = torch.linalg.norm(object_pos - target_pos, ord=2, dim=-1) - goal_resets = reset_goal_buf | goal_reached - successes = successes + goal_resets - fell = goal_distance >= fall_distance - reward = ( - goal_distance * distance_scale - + rotation_scale / (rotation_distance + rotation_epsilon) - + actions.square().sum(dim=-1) * action_penalty_scale - + goal_resets.to(goal_distance.dtype) * success_bonus - + fell.to(goal_distance.dtype) * fall_penalty - ) - resets = reset_buf | fell - num_resets = resets.sum() - finished_successes = (successes * resets).sum() - mean_successes = finished_successes / num_resets.clamp_min(1) - consecutive_successes = torch.where( - num_resets > 0, - averaging_factor * mean_successes + (1.0 - averaging_factor) * consecutive_successes, - consecutive_successes, - ) - return reward, goal_resets, successes, consecutive_successes - - class ReorientDirectEnv(DirectRLEnv): + """A dexterous hand reorients an in-hand object to a goal orientation, drawing a new goal on success.""" + cfg: AllegroHandEnvCfg | ShadowHandEnvCfg def __init__(self, cfg: AllegroHandEnvCfg | ShadowHandEnvCfg, render_mode: str | None = None, **kwargs): @@ -351,7 +282,7 @@ def _reset_idx(self, env_ids: Sequence[int]): self.successes[env_ids] = 0 self._compute_intermediate_values() - def _reset_target_pose(self, env_ids): + def _reset_target_pose(self, env_ids: Sequence[int] | torch.Tensor) -> None: # reset goal rotation rand_floats = sample_uniform(-1.0, 1.0, (len(env_ids), 2), device=self.device) new_rot = randomize_rotation( @@ -369,14 +300,13 @@ def _reset_target_pose(self, env_ids): self.reset_goal_buf[env_ids] = 0 - def _compute_intermediate_values(self): + def _compute_intermediate_values(self) -> None: """Refresh the torch-side state snapshots consumed by the observation and reward paths.""" # data for hand - self.fingertip_pos = self.hand.data.body_pos_w.torch[:, self.finger_bodies] - self.fingertip_rot = self.hand.data.body_quat_w.torch[:, self.finger_bodies] - self.fingertip_pos -= self.scene.env_origins.repeat((1, self.num_fingertips)).reshape( - self.num_envs, self.num_fingertips, 3 + self.fingertip_pos = self.hand.data.body_pos_w.torch[:, self.finger_bodies] - self.scene.env_origins.unsqueeze( + 1 ) + self.fingertip_rot = self.hand.data.body_quat_w.torch[:, self.finger_bodies] self.fingertip_velocities = self.hand.data.body_vel_w.torch[:, self.finger_bodies] self.hand_dof_pos = self.hand.data.joint_pos.torch @@ -389,12 +319,9 @@ def _compute_intermediate_values(self): self.object_linvel = self.object.data.root_lin_vel_w.torch self.object_angvel = self.object.data.root_ang_vel_w.torch - def compute_reduced_observations(self): - # Per https://arxiv.org/pdf/1808.00177.pdf Table 2 - # Fingertip positions - # Object Position, but not orientation - # Relative target orientation - obs = torch.cat( + def compute_reduced_observations(self) -> torch.Tensor: + """Fingertip positions, object position and relative goal orientation (OpenAI et al. 2018, Table 2).""" + return torch.cat( ( self.fingertip_pos.view(self.num_envs, self.num_fingertips * 3), self.object_pos, @@ -404,10 +331,9 @@ def compute_reduced_observations(self): dim=-1, ) - return obs - - def compute_full_observations(self): - obs = torch.cat( + def compute_full_observations(self) -> torch.Tensor: + """Full hand, object, goal and fingertip state followed by the last actions.""" + return torch.cat( ( # hand scale_transform(self.hand_dof_pos, self.hand_dof_lower_limits, self.hand_dof_upper_limits), @@ -430,10 +356,10 @@ def compute_full_observations(self): ), dim=-1, ) - return obs - def compute_full_state(self): - states = torch.cat( + def compute_full_state(self) -> torch.Tensor: + """Full observations plus the fingertip force-torque readings, for an asymmetric critic.""" + return torch.cat( ( # hand scale_transform(self.hand_dof_pos, self.hand_dof_lower_limits, self.hand_dof_upper_limits), @@ -458,4 +384,75 @@ def compute_full_state(self): ), dim=-1, ) - return states + + +@torch.jit.script +def reorient_reward( + reset_buf: torch.Tensor, + reset_goal_buf: torch.Tensor, + successes: torch.Tensor, + consecutive_successes: torch.Tensor, + object_pos: torch.Tensor, + target_pos: torch.Tensor, + goal_reached: torch.Tensor, + rotation_distance: torch.Tensor, + actions: torch.Tensor, + distance_scale: float, + rotation_scale: float, + rotation_epsilon: float, + action_penalty_scale: float, + success_bonus: float, + fall_distance: float, + fall_penalty: float, + averaging_factor: float, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """Compute the Direct reorientation reward and success state transition. + + The success evaluation is not recomputed here: callers pass the flags and + orientation errors computed once + per step. + + Args: + reset_buf: Current episode-reset flags. + reset_goal_buf: Current goal-reset flags. + successes: Goals reached in each episode. + consecutive_successes: Moving-average success count. + object_pos: Object positions in the environment frame [m]. + target_pos: Goal positions in the environment frame [m]. + goal_reached: Per-environment success flags for this step. + rotation_distance: Per-environment orientation errors [rad]. + actions: Normalized joint actions. + distance_scale: Position-distance reward scale [1/m]. + rotation_scale: Orientation reward scale [rad]. + rotation_epsilon: Orientation reward regularizer [rad]. + action_penalty_scale: Squared-action reward scale. + success_bonus: Reward added when a goal is reached. + fall_distance: Object-to-goal termination distance [m]. + fall_penalty: Reward added when the object is out of reach. + averaging_factor: Consecutive-success moving-average factor. + + Returns: + Reward, goal-reset flags, episode success counts, and moving-average + consecutive successes. + """ + goal_distance = torch.linalg.norm(object_pos - target_pos, ord=2, dim=-1) + goal_resets = reset_goal_buf | goal_reached + successes = successes + goal_resets + fell = goal_distance >= fall_distance + reward = ( + goal_distance * distance_scale + + rotation_scale / (rotation_distance + rotation_epsilon) + + actions.square().sum(dim=-1) * action_penalty_scale + + goal_resets.to(goal_distance.dtype) * success_bonus + + fell.to(goal_distance.dtype) * fall_penalty + ) + resets = reset_buf | fell + num_resets = resets.sum() + finished_successes = (successes * resets).sum() + mean_successes = finished_successes / num_resets.clamp_min(1) + consecutive_successes = torch.where( + num_resets > 0, + averaging_factor * mean_successes + (1.0 - averaging_factor) * consecutive_successes, + consecutive_successes, + ) + return reward, goal_resets, successes, consecutive_successes diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_manager_env_cfg.py index 405d8e1f6d66..9bb98c4475be 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_manager_env_cfg.py @@ -22,7 +22,7 @@ from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.markers import VisualizationMarkersCfg from isaaclab.scene import InteractiveSceneCfg -from isaaclab.sim.simulation_cfg import SimulationCfg +from isaaclab.sim import SimulationCfg from isaaclab.sim.spawners.materials import RigidBodyMaterialBaseCfg from isaaclab.utils import configclass from isaaclab.visualizers import VisualizerCfg @@ -30,6 +30,10 @@ import isaaclab_tasks.core.reorient.mdp as mdp from isaaclab_tasks.utils import PresetCfg +## +# Scene definition +## + @configclass class ReorientSceneBaseCfg(InteractiveSceneCfg): @@ -47,6 +51,11 @@ class ReorientSceneBaseCfg(InteractiveSceneCfg): ) +## +# MDP settings +## + + @configclass class CommandsCfg: """In-hand goal pose, re-drawn on success.""" @@ -171,6 +180,11 @@ class TerminationsCfg: ) +## +# Environment configuration +## + + @configclass class ReorientManagerEnvBaseCfg(ManagerBasedRLEnvCfg): """Manager-based reorientation with Direct-compatible semantics. @@ -188,19 +202,24 @@ class ReorientManagerEnvBaseCfg(ManagerBasedRLEnvCfg): goal_marker_cfg: VisualizationMarkersCfg = MISSING """Marker spawned at the goal pose.""" + # Basic settings observations: ObservationsCfg = ObservationsCfg() actions: ActionsCfg = ActionsCfg() commands: CommandsCfg = CommandsCfg() + # MDP settings rewards: RewardsCfg = RewardsCfg() terminations: TerminationsCfg = TerminationsCfg() + # general settings episode_length_s = 10.0 + # simulation settings sim: SimulationCfg = SimulationCfg( dt=1 / 120, physics_material=RigidBodyMaterialBaseCfg(static_friction=1.0, dynamic_friction=1.0), ) def __post_init__(self): + """Post initialization.""" for group in vars(self.observations).values(): for name in ("fingertip_pose", "fingertip_vel"): term = getattr(group, name, None) @@ -212,8 +231,7 @@ def __post_init__(self): self.commands.object_pose.orientation_success_threshold = self.goal_orientation_threshold self.commands.object_pose.goal_pose_visualizer_cfg = self.goal_marker_cfg self.sim.render_interval = self.decimation - # Frame the hand, which lies horizontal around (0, -0.25, 0.51). Looking at the origin - # from 2 m away renders a 20 cm hand a few pixels wide, so a recorded video shows nothing. + # visualizer settings: frame the hand, which lies horizontal around (0, -0.25, 0.51) self.sim.default_visualizer_cfg = VisualizerCfg( eye=(0.62, -0.80, 0.85), lookat=(0.0, -0.28, 0.53), focal_length=35.0 ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/utils.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/utils.py index 2f825de7e830..a88ac10cafaf 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/utils.py @@ -8,7 +8,10 @@ The hand-over task also uses these, as it already builds on this package's MDP terms. """ +from __future__ import annotations + from collections.abc import Sequence +from typing import TYPE_CHECKING import numpy as np import torch @@ -16,6 +19,9 @@ import isaaclab.utils.math as math_utils from isaaclab.utils.math import quat_from_angle_axis, quat_mul +if TYPE_CHECKING: + from isaaclab.assets import Articulation + class EpisodeErrorRecorder: """Record the minimum physical error reached in each episode. @@ -243,7 +249,7 @@ def randomize_rotation( def resolve_actuated_tendons( - hand, + hand: Articulation, tendon_names: Sequence[str], num_envs: int, device: str, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/__init__.py index 775381be0427..1038a9b17e49 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/__init__.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""ANYmal-D velocity-tracking locomotion environments.""" + import gymnasium as gym from . import agents diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/flat_env_cfg.py index 6cebcbc62fd4..d9c0c0263bb1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/flat_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the ANYmal-D velocity-tracking environment on flat terrain.""" + from isaaclab.utils import configclass from .rough_env_cfg import AnymalDRoughEnvCfg @@ -10,6 +12,8 @@ @configclass class AnymalDFlatEnvCfg(AnymalDRoughEnvCfg): + """Configuration for the ANYmal-D velocity-tracking environment on flat terrain.""" + def __post_init__(self): super().__post_init__() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/rough_env_cfg.py index 69bebc876764..b50e6a5a2058 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/rough_env_cfg.py @@ -3,19 +3,19 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the ANYmal-D velocity-tracking environment on rough terrain.""" from isaaclab.utils import configclass from isaaclab_tasks.core.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg -## -# Pre-defined configs -## -from isaaclab_assets.robots.anymal import ANYMAL_D_CFG # isort: skip +from isaaclab_assets.robots.anymal import ANYMAL_D_CFG @configclass class AnymalDRoughEnvCfg(LocomotionVelocityRoughEnvCfg): + """Configuration for the ANYmal-D velocity-tracking environment on rough terrain.""" + def __post_init__(self): super().__post_init__() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/__init__.py index ea04e8ebf93b..e10ae36bc1c0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/__init__.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Cassie velocity-tracking locomotion environments.""" + import gymnasium as gym from . import agents diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/flat_env_cfg.py index ceac57ab74d9..73804d550de0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/flat_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the Cassie velocity-tracking environment on flat terrain.""" + from isaaclab.utils import configclass from .rough_env_cfg import CassieRoughEnvCfg @@ -10,6 +12,8 @@ @configclass class CassieFlatEnvCfg(CassieRoughEnvCfg): + """Configuration for the Cassie velocity-tracking environment on flat terrain.""" + def __post_init__(self): super().__post_init__() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/rough_env_cfg.py index f65b6af462a1..d76d168f678e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/rough_env_cfg.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the Cassie velocity-tracking environment on rough terrain.""" from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.managers import SceneEntityCfg @@ -14,14 +15,13 @@ RewardsCfg, ) -## -# Pre-defined configs -## -from isaaclab_assets.robots.cassie import CASSIE_CFG # isort: skip +from isaaclab_assets.robots.cassie import CASSIE_CFG @configclass class CassieRewardsCfg(RewardsCfg): + """Reward terms for the MDP.""" + termination_penalty = RewTerm(func=mdp.is_terminated, weight=-200.0) feet_air_time = RewTerm( func=mdp.feet_air_time_positive_biped, @@ -52,6 +52,8 @@ class CassieRewardsCfg(RewardsCfg): @configclass class CassieRoughEnvCfg(LocomotionVelocityRoughEnvCfg): + """Configuration for the Cassie velocity-tracking environment on rough terrain.""" + rewards: CassieRewardsCfg = CassieRewardsCfg() def __post_init__(self): @@ -74,7 +76,7 @@ def __post_init__(self): # terminations self.terminations.base_contact.params["sensor_cfg"].body_names = [".*pelvis"] # events - # asymmetric pelvis mass scale (1.0, 1.25) — a lighter-than-nominal pelvis destabilizes Cassie + # asymmetric pelvis mass scale (1.0, 1.25): a lighter-than-nominal pelvis destabilizes Cassie self.events.add_base_mass.params["asset_cfg"].body_names = "pelvis" self.events.add_base_mass.params["mass_distribution_params"] = (1.0, 1.25) self.events.base_com = None diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/__init__.py index c812894c8de7..41304bc72775 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/__init__.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Unitree G1 velocity-tracking locomotion environments.""" + import gymnasium as gym from . import agents @@ -12,26 +14,25 @@ ## gym.register( - id="Isaac-Velocity-Rough-G1", + id="Isaac-Velocity-Flat-G1", entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.rough_env_cfg:G1RoughEnvCfg", - "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:G1RoughPPORunnerCfg", + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:G1FlatEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:G1FlatPPORunnerCfg", "default_agent": "rsl_rl", - "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, ) - gym.register( - id="Isaac-Velocity-Flat-G1", + id="Isaac-Velocity-Rough-G1", entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.flat_env_cfg:G1FlatEnvCfg", - "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:G1FlatPPORunnerCfg", + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:G1RoughEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:G1RoughPPORunnerCfg", "default_agent": "rsl_rl", - "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/flat_env_cfg.py index 300c45950351..45086e8054e2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/flat_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the Unitree G1 velocity-tracking environment on flat terrain.""" + from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass @@ -11,6 +13,8 @@ @configclass class G1FlatEnvCfg(G1RoughEnvCfg): + """Configuration for the Unitree G1 velocity-tracking environment on flat terrain.""" + def __post_init__(self): super().__post_init__() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/rough_env_cfg.py index b489b603e02c..affd57efde7b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/rough_env_cfg.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the Unitree G1 velocity-tracking environment on rough terrain.""" from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.managers import SceneEntityCfg @@ -14,10 +15,7 @@ RewardsCfg, ) -## -# Pre-defined configs -## -from isaaclab_assets import G1_MINIMAL_CFG # isort: skip +from isaaclab_assets import G1_MINIMAL_CFG @configclass @@ -106,6 +104,8 @@ class G1Rewards(RewardsCfg): @configclass class G1RoughEnvCfg(LocomotionVelocityRoughEnvCfg): + """Configuration for the Unitree G1 velocity-tracking environment on rough terrain.""" + rewards: G1Rewards = G1Rewards() def __post_init__(self): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/__init__.py index 4e4c8c9744a0..c6105131f27a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/__init__.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Unitree Go2 velocity-tracking locomotion environments.""" + import gymnasium as gym from . import agents diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/flat_env_cfg.py index b89c7af23b38..46f3f9effadc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/flat_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the Unitree Go2 velocity-tracking environment on flat terrain.""" + from isaaclab.utils import configclass from .rough_env_cfg import UnitreeGo2RoughEnvCfg @@ -10,6 +12,8 @@ @configclass class UnitreeGo2FlatEnvCfg(UnitreeGo2RoughEnvCfg): + """Configuration for the Unitree Go2 velocity-tracking environment on flat terrain.""" + def __post_init__(self): super().__post_init__() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/rough_env_cfg.py index ed7e260714f8..f61ecf82b80b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/rough_env_cfg.py @@ -3,19 +3,19 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the Unitree Go2 velocity-tracking environment on rough terrain.""" from isaaclab.utils import configclass from isaaclab_tasks.core.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg -## -# Pre-defined configs -## -from isaaclab_assets.robots.unitree import UNITREE_GO2_CFG # isort: skip +from isaaclab_assets.robots.unitree import UNITREE_GO2_CFG @configclass class UnitreeGo2RoughEnvCfg(LocomotionVelocityRoughEnvCfg): + """Configuration for the Unitree Go2 velocity-tracking environment on rough terrain.""" + def __post_init__(self): super().__post_init__() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/__init__.py index d2fca850d2c4..882a9667f244 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/__init__.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Unitree H1 velocity-tracking locomotion environments.""" + import gymnasium as gym from . import agents @@ -12,26 +14,25 @@ ## gym.register( - id="Isaac-Velocity-Rough-H1", + id="Isaac-Velocity-Flat-H1", entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.rough_env_cfg:H1RoughEnvCfg", - "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:H1RoughPPORunnerCfg", + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:H1FlatEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:H1FlatPPORunnerCfg", "default_agent": "rsl_rl", - "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, ) - gym.register( - id="Isaac-Velocity-Flat-H1", + id="Isaac-Velocity-Rough-H1", entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.flat_env_cfg:H1FlatEnvCfg", - "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:H1FlatPPORunnerCfg", + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:H1RoughEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:H1RoughPPORunnerCfg", "default_agent": "rsl_rl", - "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/flat_env_cfg.py index caa871480a40..1db600e46532 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/flat_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the Unitree H1 velocity-tracking environment on flat terrain.""" + from isaaclab.utils import configclass from .rough_env_cfg import H1RoughEnvCfg @@ -10,6 +12,8 @@ @configclass class H1FlatEnvCfg(H1RoughEnvCfg): + """Configuration for the Unitree H1 velocity-tracking environment on flat terrain.""" + def __post_init__(self): super().__post_init__() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/rough_env_cfg.py index 4ee17887b912..7a757832c46b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/rough_env_cfg.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Configuration for the Unitree H1 velocity-tracking environment on rough terrain.""" from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.managers import SceneEntityCfg @@ -14,10 +15,7 @@ RewardsCfg, ) -## -# Pre-defined configs -## -from isaaclab_assets import H1_MINIMAL_CFG # isort: skip +from isaaclab_assets import H1_MINIMAL_CFG @configclass @@ -73,6 +71,8 @@ class H1Rewards(RewardsCfg): @configclass class H1RoughEnvCfg(LocomotionVelocityRoughEnvCfg): + """Configuration for the Unitree H1 velocity-tracking environment on rough terrain.""" + rewards: H1Rewards = H1Rewards() def __post_init__(self): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/__init__.py index 8f9a146abdc8..e5a6ad09e23e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/__init__.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module contains the functions that are specific to the locomotion environments.""" +"""MDP terms specific to the velocity-tracking locomotion environments.""" from isaaclab.utils.module import lazy_export diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/__init__.pyi index 653328b4faaf..025fecca866a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/__init__.pyi @@ -4,14 +4,14 @@ # SPDX-License-Identifier: BSD-3-Clause __all__ = [ - "terrain_levels_vel", "feet_air_time", "feet_air_time_positive_biped", "feet_slide", "stand_still_joint_deviation_l1", + "terrain_levels_vel", + "terrain_out_of_bounds", "track_ang_vel_z_world_exp", "track_lin_vel_xy_yaw_frame_exp", - "terrain_out_of_bounds", ] from .curriculums import terrain_levels_vel @@ -24,4 +24,5 @@ from .rewards import ( track_lin_vel_xy_yaw_frame_exp, ) from .terminations import terrain_out_of_bounds + from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/curriculums.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/curriculums.py index 1438cbf03801..92b4d4fb2dcd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/curriculums.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/curriculums.py @@ -3,11 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Common functions that can be used to create curriculum for the learning environment. - -The functions can be passed to the :class:`isaaclab.managers.CurriculumTermCfg` object to enable -the curriculum introduced by the function. -""" +"""Curriculum terms for the velocity-tracking locomotion environments.""" from __future__ import annotations diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/rewards.py index 21bf4d1c7083..45a9a6b42125 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/rewards.py @@ -3,11 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Common functions that can be used to define rewards for the learning environment. - -The functions can be passed to the :class:`isaaclab.managers.RewardTermCfg` object to -specify the reward function and its parameters. -""" +"""Reward terms for the velocity-tracking locomotion environments.""" from __future__ import annotations @@ -20,6 +16,7 @@ from isaaclab.utils.math import quat_apply_inverse, yaw_quat if TYPE_CHECKING: + from isaaclab.assets import Articulation from isaaclab.envs import ManagerBasedRLEnv from isaaclab.sensors import ContactSensor @@ -46,7 +43,9 @@ def feet_air_time( return reward -def feet_air_time_positive_biped(env, command_name: str, threshold: float, sensor_cfg: SceneEntityCfg) -> torch.Tensor: +def feet_air_time_positive_biped( + env: ManagerBasedRLEnv, command_name: str, threshold: float, sensor_cfg: SceneEntityCfg +) -> torch.Tensor: """Reward long steps taken by the feet for bipeds. This function rewards the agent for taking steps up to a specified threshold and also keep one foot at @@ -68,34 +67,30 @@ def feet_air_time_positive_biped(env, command_name: str, threshold: float, senso return reward -def feet_slide(env, sensor_cfg: SceneEntityCfg, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: +def feet_slide( + env: ManagerBasedRLEnv, sensor_cfg: SceneEntityCfg, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: """Penalize feet sliding. This function penalizes the agent for sliding its feet on the ground. The reward is computed as the norm of the linear velocity of the feet multiplied by a binary contact sensor. This ensures that the agent is penalized only when the feet are in contact with the ground. """ - # Penalize feet sliding contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] + asset: Articulation = env.scene[asset_cfg.name] contacts = ( contact_sensor.data.net_normal_forces_w_history.torch[:, :, sensor_cfg.body_ids, :].norm(dim=-1).max(dim=1)[0] > 1.0 ) - asset = env.scene[asset_cfg.name] - body_vel = asset.data.body_lin_vel_w.torch[:, asset_cfg.body_ids, :2] - reward = torch.sum(body_vel.norm(dim=-1) * contacts, dim=1) - return reward + return torch.sum(body_vel.norm(dim=-1) * contacts, dim=1) def track_lin_vel_xy_yaw_frame_exp( - env, std: float, command_name: str, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") + env: ManagerBasedRLEnv, std: float, command_name: str, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") ) -> torch.Tensor: - """Reward tracking of linear velocity commands (xy axes) in the gravity aligned - robot frame using an exponential kernel. - """ - # extract the used quantities (to enable type-hinting) - asset = env.scene[asset_cfg.name] + """Reward tracking of linear velocity commands (xy axes) in the gravity-aligned robot frame with an exp kernel.""" + asset: Articulation = env.scene[asset_cfg.name] vel_yaw = quat_apply_inverse(yaw_quat(asset.data.root_quat_w.torch), asset.data.root_lin_vel_w.torch[:, :3]) lin_vel_error = torch.sum( torch.square(env.command_manager.get_command(command_name)[:, :2] - vel_yaw[:, :2]), dim=1 @@ -104,11 +99,10 @@ def track_lin_vel_xy_yaw_frame_exp( def track_ang_vel_z_world_exp( - env, command_name: str, std: float, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") + env: ManagerBasedRLEnv, command_name: str, std: float, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") ) -> torch.Tensor: """Reward tracking of angular velocity commands (yaw) in world frame using exponential kernel.""" - # extract the used quantities (to enable type-hinting) - asset = env.scene[asset_cfg.name] + asset: Articulation = env.scene[asset_cfg.name] ang_vel_error = torch.square( env.command_manager.get_command(command_name)[:, 2] - asset.data.root_ang_vel_w.torch[:, 2] ) @@ -116,7 +110,10 @@ def track_ang_vel_z_world_exp( def stand_still_joint_deviation_l1( - env, command_name: str, command_threshold: float = 0.06, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") + env: ManagerBasedRLEnv, + command_name: str, + command_threshold: float = 0.06, + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: """Penalize offsets from the default joint positions when the command is very small.""" command = env.command_manager.get_command(command_name) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/symmetry/anymal.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/symmetry/anymal.py index f4197ccbe76e..4a1bc3aa9b20 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/symmetry/anymal.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/symmetry/anymal.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: BSD-3-Clause - """Functions to specify the symmetry in the observation and action space for ANYmal.""" from __future__ import annotations diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/terminations.py index 5559250d9555..37559032495a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/mdp/terminations.py @@ -3,11 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Common functions that can be used to activate certain terminations. - -The functions can be passed to the :class:`isaaclab.managers.TerminationTermCfg` object to enable -the termination introduced by the function. -""" +"""Termination terms for the velocity-tracking locomotion environments.""" from __future__ import annotations diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/velocity_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/velocity_env_cfg.py index 7aeae7f961fb..e86affbfe86e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/velocity_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/velocity_env_cfg.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Base configuration for the velocity-tracking locomotion environments on rough terrain.""" + import math from dataclasses import MISSING @@ -31,6 +33,7 @@ from isaaclab.sensors import ContactSensorCfg, RayCasterCfg, patterns from isaaclab.sim import SimulationCfg from isaaclab.terrains import TerrainImporterCfg +from isaaclab.terrains.config.rough import ROUGH_TERRAINS_CFG from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR from isaaclab.utils.noise import UniformNoiseCfg as Unoise @@ -39,13 +42,7 @@ from isaaclab_tasks.utils import PresetCfg ## -# Pre-defined configs -## -from isaaclab.terrains.config.rough import ROUGH_TERRAINS_CFG # isort: skip - - -## -# Physics presets +# Physics backend presets ## @@ -105,9 +102,8 @@ class MySceneCfg(InteractiveSceneCfg): ) # robots robot: ArticulationCfg = MISSING - # sensors -- the concrete implementation is selected automatically from the active physics - # backend (Newton / PhysX / OvPhysX); backend-specific fields such as ``global_world_only`` are - # documented on the config and ignored by the backends that do not use them. + # sensors: the concrete implementation is selected automatically from the active physics backend; + # backend-specific fields such as ``global_world_only`` are ignored by the backends that do not use them height_scanner = RayCasterCfg( prim_path="{ENV_REGEX_NS}/Robot/base", offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)), @@ -214,9 +210,7 @@ class EventsCfg: mode="startup", params={ "asset_cfg": SceneEntityCfg("robot", body_names="base"), - # Multiplicative ±25% log-uniform. Scale-invariant across robot sizes - # (no per-robot kg overrides needed) with geometric mean 1.0 and - # symmetric inverse perturbation (acceleration symmetric around nominal). + # multiplicative +-25% log-uniform: scale-invariant across robot sizes with geometric mean 1.0 "mass_distribution_params": (1 / 1.25, 1.25), "operation": "scale", "distribution": "log_uniform", @@ -340,7 +334,7 @@ class CurriculumCfg: class LocomotionVelocityRoughEnvCfg(ManagerBasedRLEnvCfg): """Configuration for the locomotion velocity-tracking environment.""" - # Simulation settings — shared physics preset (PhysX + MJWarp) for all rough-terrain envs + # Simulation settings sim: SimulationCfg = SimulationCfg(physics=RoughPhysicsCfg()) # Scene settings scene: MySceneCfg = MySceneCfg(num_envs=4096, env_spacing=2.5) diff --git a/source/isaaclab_tasks/test/benchmarking/configs.yaml b/source/isaaclab_tasks/test/benchmarking/configs.yaml deleted file mode 100644 index 6045b96e7f1d..000000000000 --- a/source/isaaclab_tasks/test/benchmarking/configs.yaml +++ /dev/null @@ -1,436 +0,0 @@ -# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - - -# mode for very simple functional testing without checking thresholds -fast_test: - rl_games:Isaac-Ant: - max_iterations: 10 - lower_thresholds: - reward: -99999 - episode_length: -99999 - upper_thresholds: - duration: 99999 - -# mode for capturing KPIs across all environments without checking thresholds -full_test: - Isaac-*: - max_iterations: 10 - lower_thresholds: - reward: -99999 - episode_length: -99999 - upper_thresholds: - duration: 99999 - -# mode for PR tests (default mode) -fast: - rl_games:Isaac-Ant: - max_iterations: 200 - lower_thresholds: - reward: 45 - episode_length: 900 - upper_thresholds: - duration: 1200 #750 - skrl:Isaac-Cartpole-Camera-Direct: - max_iterations: 50 - lower_thresholds: - reward: 190 - episode_length: 230 - upper_thresholds: - duration: 450 - rsl_rl:Isaac-Humanoid: - max_iterations: 200 - lower_thresholds: - reward: 50 - episode_length: 600 #700 - upper_thresholds: - duration: 500 - skrl:Isaac-Shadow-Handover-Direct: - max_iterations: 300 - lower_thresholds: - reward: 30 - episode_length: 250 - upper_thresholds: - duration: 600 - rsl_rl:IsaacContrib-Velocity-Rough-AnymalC: - max_iterations: 300 - lower_thresholds: - reward: 7 - episode_length: 875 - upper_thresholds: - duration: 1800 - sb3:IsaacContrib-Lift-Cube-Franka: - max_iterations: 150 - lower_thresholds: - reward: 1.5 - episode_length: 150 - upper_thresholds: - duration: 2000 - rsl_rl:Isaac-Lift-KukaAllegro: - max_iterations: 300 # 200 - lower_thresholds: - reward: 0.4 # 0.7 - episode_length: 150 - upper_thresholds: - duration: 1400 #1200 - rsl_rl:IsaacContrib-Reach-OpenArm: - max_iterations: 200 - lower_thresholds: - reward: 0.15 - episode_length: 100 - upper_thresholds: - duration: 600 - -# mode for nightly CI -full: - # Locomotion tasks - basic - Isaac-Ant-Direct: - max_iterations: 300 - lower_thresholds: - reward: 7000 - episode_length: 700 - upper_thresholds: - duration: 500 - Isaac-Ant: - max_iterations: 1000 - lower_thresholds: - reward: 100 - episode_length: 700 - upper_thresholds: - duration: 800 - Isaac-Humanoid-Direct: - max_iterations: 300 - lower_thresholds: - reward: 2000 - episode_length: 600 - upper_thresholds: - duration: 1000 - Isaac-Humanoid: - max_iterations: 1000 - lower_thresholds: - reward: 100 - episode_length: 600 - upper_thresholds: - duration: 2500 - # Classic control tasks - Isaac-Pendulum-MARL-Direct: - max_iterations: 300 - lower_thresholds: - reward: 30.3 - episode_length: 150 - upper_thresholds: - duration: 500 - Isaac-Cartpole-Direct: - max_iterations: 300 - lower_thresholds: - reward: 200 - episode_length: 150 - upper_thresholds: - duration: 500 - Isaac-Cartpole: - max_iterations: 1000 - lower_thresholds: - reward: 3 - episode_length: 150 - upper_thresholds: - duration: 1500 - # Vision-based cartpole tasks - Isaac-Cartpole-Camera-Direct: - max_iterations: 300 - lower_thresholds: - reward: 200 - episode_length: 150 - upper_thresholds: - duration: 3000 - Isaac-Cartpole-Camera: - max_iterations: 300 - lower_thresholds: - reward: -2 - episode_length: 150 - upper_thresholds: - duration: 4000 - # Manipulation tasks - Franka - Isaac-Open-Drawer-Franka-Direct: - max_iterations: 200 - lower_thresholds: - reward: 60 - episode_length: 150 - upper_thresholds: - duration: 3000 - IsaacContrib-Lift-Cube-Franka: - max_iterations: 300 - lower_thresholds: - reward: 90 - episode_length: 100 - upper_thresholds: - duration: 1000 - Isaac-Open-Drawer-Franka: - max_iterations: 200 - lower_thresholds: - reward: 60 - episode_length: 150 - upper_thresholds: - duration: 3000 - Isaac-Reach-Franka-*: - max_iterations: 1000 - lower_thresholds: - reward: 0.25 - episode_length: 150 - upper_thresholds: - duration: 1500 - Isaac-Reach-Franka-OSC: - max_iterations: 1000 - lower_thresholds: - reward: 0.25 - episode_length: 150 - upper_thresholds: - duration: 1500 - # Manipulation tasks - OpenArm - IsaacContrib-Lift-Cube-OpenArm: - max_iterations: 300 - lower_thresholds: - reward: 80 - episode_length: 100 - upper_thresholds: - duration: 1200 - IsaacContrib-Open-Drawer-OpenArm: - max_iterations: 200 - lower_thresholds: - reward: 50 - episode_length: 150 - upper_thresholds: - duration: 3000 - IsaacContrib-Reach-OpenArm: - max_iterations: 1000 - lower_thresholds: - reward: 0.25 - episode_length: 150 - upper_thresholds: - duration: 1500 - IsaacContrib-Reach-OpenArmBi: - max_iterations: 1000 - lower_thresholds: - reward: 0.25 - episode_length: 150 - upper_thresholds: - duration: 1800 - # Manipulation tasks - UR10 - Isaac-Reach-UR10: - max_iterations: 1000 - lower_thresholds: - reward: 0.25 - episode_length: 150 - upper_thresholds: - duration: 1500 - # Dexterous manipulation - Allegro hand - Isaac-Reorient-Cube-Allegro-Direct: - max_iterations: 500 - lower_thresholds: - reward: 200 - episode_length: 150 - upper_thresholds: - duration: 1500 - Isaac-Reorient-Cube-Allegro*: - max_iterations: 500 - lower_thresholds: - reward: 15 - episode_length: 300 - upper_thresholds: - duration: 1500 - # Dexterous manipulation - Shadow hand - Isaac-Reorient-Cube-Shadow-Direct: - max_iterations: 3000 - lower_thresholds: - reward: 1000 - episode_length: 300 - upper_thresholds: - duration: 10000 - IsaacContrib-Reorient-Cube-Shadow-OpenAI-FF-Direct: - max_iterations: 3000 - lower_thresholds: - reward: 1000 - episode_length: 50 - upper_thresholds: - duration: 15000 - IsaacContrib-Reorient-Cube-Shadow-OpenAI-LSTM-Direct: - max_iterations: 3000 - lower_thresholds: - reward: 1000 - episode_length: 100 - upper_thresholds: - duration: 30000 - Isaac-Reorient-Cube-Shadow-Camera-Direct: - max_iterations: 3000 - lower_thresholds: - reward: 1000 - episode_length: 400 - upper_thresholds: - duration: 40000 - Isaac-Shadow-Handover-Direct: - max_iterations: 3000 - lower_thresholds: - reward: 1000 - episode_length: 150 - upper_thresholds: - duration: 10000 - # Dexterous manipulation - KukaAllegro - Isaac-Lift-KukaAllegro: - max_iterations: 500 - lower_thresholds: - reward: 50 - episode_length: 200 - upper_thresholds: - duration: 2000 - Isaac-Reorient-KukaAllegro: - max_iterations: 500 - lower_thresholds: - reward: 50 - episode_length: 200 - upper_thresholds: - duration: 2000 - # Factory and forge tasks - Isaac-AutoMate-Assembly-Direct: - max_iterations: 300 - lower_thresholds: - reward: 100 - episode_length: 200 - upper_thresholds: - duration: 1500 - Isaac-AutoMate-Disassembly-Direct: - max_iterations: 300 - lower_thresholds: - reward: 100 - episode_length: 200 - upper_thresholds: - duration: 1500 - Isaac-Factory-GearMesh-Direct: - max_iterations: 100 - lower_thresholds: - reward: 200 - episode_length: 250 - upper_thresholds: - duration: 6000 - Isaac-Factory-NutThread-Direct: - max_iterations: 100 - lower_thresholds: - reward: 400 - episode_length: 400 - upper_thresholds: - duration: 5000 - Isaac-Factory-PegInsert-Direct: - max_iterations: 100 - lower_thresholds: - reward: 125 - episode_length: 130 - upper_thresholds: - duration: 4000 - Isaac-Forge-GearMesh-Direct: - max_iterations: 100 - lower_thresholds: - reward: 200 - episode_length: 250 - upper_thresholds: - duration: 6000 - Isaac-Forge-NutThread-Direct: - max_iterations: 100 - lower_thresholds: - reward: 400 - episode_length: 400 - upper_thresholds: - duration: 5000 - Isaac-Forge-PegInsert-Direct: - max_iterations: 100 - lower_thresholds: - reward: 125 - episode_length: 130 - upper_thresholds: - duration: 4000 - # Quadruped and humanoid locomotion - flat terrain - Isaac-Velocity-Flat-*: - max_iterations: 1000 - lower_thresholds: - reward: 15 - episode_length: 700 - upper_thresholds: - duration: 3000 - IsaacContrib-Velocity-Flat-Spot: - max_iterations: 1000 - lower_thresholds: - reward: 150 - episode_length: 700 - upper_thresholds: - duration: 6000 - IsaacContrib-Velocity-Flat-Digit: - max_iterations: 1000 - lower_thresholds: - reward: 10 - episode_length: 700 - upper_thresholds: - duration: 3000 - Isaac-Velocity-Flat-G1: - max_iterations: 1000 - lower_thresholds: - reward: 10 - episode_length: 700 - upper_thresholds: - duration: 3500 - IsaacContrib-Velocity-Flat-UnitreeGo1: - max_iterations: 1000 - lower_thresholds: - reward: 15 - episode_length: 700 - upper_thresholds: - duration: 3000 - Isaac-Velocity-Flat-UnitreeGo2: - max_iterations: 1000 - lower_thresholds: - reward: 15 - episode_length: 700 - upper_thresholds: - duration: 3000 - # Quadruped and humanoid locomotion - rough terrain - Isaac-Velocity-Rough-*: - max_iterations: 1000 - lower_thresholds: - reward: 7 - episode_length: 700 - upper_thresholds: - duration: 6000 - IsaacContrib-Velocity-Rough-Digit: - max_iterations: 1000 - lower_thresholds: - reward: 5 - episode_length: 700 - upper_thresholds: - duration: 6000 - Isaac-Velocity-Rough-G1: - max_iterations: 1000 - lower_thresholds: - reward: 5 - episode_length: 700 - upper_thresholds: - duration: 6000 - IsaacContrib-Velocity-Rough-UnitreeGo1: - max_iterations: 1000 - lower_thresholds: - reward: 7 - episode_length: 700 - upper_thresholds: - duration: 6000 - Isaac-Velocity-Rough-UnitreeGo2: - max_iterations: 1000 - lower_thresholds: - reward: 7 - episode_length: 700 - upper_thresholds: - duration: 6000 - # Locomotion-manipulation - IsaacContrib-Tracking-LocoManip-Digit: - max_iterations: 500 - lower_thresholds: - reward: 5 - episode_length: 500 - upper_thresholds: - duration: 3000 diff --git a/source/isaaclab_tasks/test/benchmarking/conftest.py b/source/isaaclab_tasks/test/benchmarking/conftest.py deleted file mode 100644 index 51096c6de144..000000000000 --- a/source/isaaclab_tasks/test/benchmarking/conftest.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -import json -import os -from datetime import datetime - -import pytest - -# Local imports should be imported last -import env_benchmark_test_utils as utils # isort: skip - -# Global variable for storing KPI data -GLOBAL_KPI_STORE = {} -# Global variable for storing the start timestamp -START_TIMESTAMP = None - - -def pytest_addoption(parser): - parser.addoption( - "--workflows", - action="store", - nargs="+", - default=["rl_games", "rsl_rl", "sb3", "skrl"], - help="List of workflows. Must be equal to or a subset of the default list.", - ) - parser.addoption( - "--config_path", - action="store", - default="configs.yaml", - help="Path to config file for environment training and evaluation.", - ) - parser.addoption( - "--mode", - action="store", - default="fast", - help="Coverage mode defined in the config file.", - ) - parser.addoption("--num_gpus", action="store", type=int, default=1, help="Number of GPUs for distributed training.") - parser.addoption( - "--save_kpi_payload", - action="store_true", - help="To collect output metrics into a KPI payload that can be uploaded to a dashboard.", - ) - parser.addoption( - "--tag", - action="store", - default="", - help="Optional tag to add to the KPI payload for filtering on the Grafana dashboard.", - ) - - -@pytest.fixture -def workflows(request): - return request.config.getoption("--workflows") - - -@pytest.fixture -def config_path(request): - return request.config.getoption("--config_path") - - -@pytest.fixture -def mode(request): - return request.config.getoption("--mode") - - -@pytest.fixture -def num_gpus(request): - return request.config.getoption("--num_gpus") - - -@pytest.fixture -def save_kpi_payload(request): - return request.config.getoption("--save_kpi_payload") - - -@pytest.fixture -def tag(request): - return request.config.getoption("--tag") - - -# Fixture for storing KPI data in a global variable -@pytest.fixture(scope="session") -def kpi_store(): - return GLOBAL_KPI_STORE # Using global variable for storing KPI data - - -# Shard parametrized test items across parallel CI jobs. -# Reads the same TEST_SHARD_INDEX / TEST_SHARD_COUNT env vars used by tools/conftest.py -# for file-level sharding, but applies them at the test-item level so a single -# parametrized file can be split across multiple runners. -# This is a pytest hook — pytest calls it automatically during test collection. -def pytest_collection_modifyitems(config, items): - shard_index = os.environ.get("TEST_SHARD_INDEX", "") - shard_count = os.environ.get("TEST_SHARD_COUNT", "") - if shard_index and shard_count: - shard_index = int(shard_index) - shard_count = int(shard_count) - items[:] = [item for i, item in enumerate(items) if i % shard_count == shard_index] - print(f"Shard {shard_index}/{shard_count}: selected {len(items)} test items") - - -# This hook dynamically generates test cases based on the --workflows option. -# For any test that includes a 'workflow' fixture, this will parametrize it -# with all values passed via the command line option --workflows. -def pytest_generate_tests(metafunc): - if "workflow" in metafunc.fixturenames: - workflows = metafunc.config.getoption("workflows") - metafunc.parametrize("workflow", workflows) - - -# The pytest session start hook to capture the start timestamp -def pytest_sessionstart(session): - global START_TIMESTAMP - START_TIMESTAMP = datetime.now().isoformat() - - -# The pytest session finish hook -def pytest_sessionfinish(session, exitstatus): - # Access global variable instead of fixture - tag = session.config.getoption("--tag") - utils.process_kpi_data(GLOBAL_KPI_STORE, tag=tag, timestamp=START_TIMESTAMP) - print(json.dumps(GLOBAL_KPI_STORE, indent=2)) - save_kpi_payload = session.config.getoption("--save_kpi_payload") - if save_kpi_payload: - print("Saving KPI data...") - utils.output_payloads(GLOBAL_KPI_STORE) diff --git a/source/isaaclab_tasks/test/benchmarking/env_benchmark_test_utils.py b/source/isaaclab_tasks/test/benchmarking/env_benchmark_test_utils.py deleted file mode 100644 index acfd84770ed9..000000000000 --- a/source/isaaclab_tasks/test/benchmarking/env_benchmark_test_utils.py +++ /dev/null @@ -1,274 +0,0 @@ -# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -import glob -import json -import math -import os -import re - -import numpy as np -import yaml - - -def _get_repo_path(): - """Get the repository root by searching for marker files. - - Searches upward from the current file for IsaacLab repository markers - (isaaclab.sh or pyproject.toml) to robustly find the repo root. - """ - current = os.path.abspath(__file__) - # Look for isaaclab.sh or pyproject.toml as markers (max 10 levels up) - for _ in range(10): - current = os.path.dirname(current) - if os.path.exists(os.path.join(current, "isaaclab.sh")): - return current - # Fallback marker - if os.path.exists(os.path.join(current, "pyproject.toml")) and os.path.exists(os.path.join(current, "source")): - return current - raise RuntimeError("Could not find IsaacLab repository root. Expected to find 'isaaclab.sh' in parent directories.") - - -def get_env_configs(configs_path): - """Get environment configurations from yaml filepath.""" - with open(configs_path) as env_configs_file: - env_configs = yaml.safe_load(env_configs_file) - return env_configs - - -def get_env_config(env_configs, mode, workflow, task): - """Get the environment configuration.""" - if mode not in env_configs: - raise ValueError(f"Mode {mode} is not supported in the config file.") - - extended_task = f"{workflow}:{task}" - # return a direct match with extended task name - if extended_task in env_configs[mode]: - return env_configs[mode][extended_task] - - # else, return a direct match with task name - if task in env_configs[mode]: - return env_configs[mode][task] - - # else, return a regex match with extended task name - for env_config_key in env_configs[mode].keys(): - if re.match(env_config_key, extended_task): - return env_configs[mode][env_config_key] - - # else, return a regex match with task name - for env_config_key in env_configs[mode].keys(): - if re.match(env_config_key, task): - return env_configs[mode][env_config_key] - - # if no match is found, return None - return None - - -def evaluate_job(workflow, task, env_config, duration): - """Evaluate the job.""" - log_data = _retrieve_logs(workflow, task) - - kpi_payload = {"success": True, "msg": ""} - - # handle case where no log files are found - if not log_data: - kpi_payload["success"] = False - kpi_payload["msg"] = "error: training did not finish!" - return kpi_payload - - thresholds = {**env_config.get("lower_thresholds", {}), **env_config.get("upper_thresholds", {})} - - # evaluate all thresholds from the config - for threshold_name, threshold_val in thresholds.items(): - uses_lower_threshold = threshold_name in env_config.get("lower_thresholds", {}) - if threshold_name == "duration": - val = duration - else: - val = _extract_log_val(threshold_name, log_data, uses_lower_threshold, workflow) - # skip non-numeric values - if val is None or not isinstance(val, (int, float)) or (isinstance(val, float) and math.isnan(val)): - continue - val = round(val, 4) - threshold_val_rounded = round(threshold_val, 4) - if uses_lower_threshold: - if val < threshold_val: - kpi_payload["success"] = False - if not kpi_payload["msg"]: - kpi_payload["msg"] = f"{threshold_name} below threshold: {val} < {threshold_val_rounded}" - else: - if val > threshold_val: - kpi_payload["success"] = False - if not kpi_payload["msg"]: - kpi_payload["msg"] = f"{threshold_name} above threshold: {val} > {threshold_val_rounded}" - kpi_payload[threshold_name] = val - if threshold_name == "reward": - normalized_reward = val / threshold_val - kpi_payload[f"{threshold_name}_normalized"] = normalized_reward - kpi_payload[f"{threshold_name}_threshold"] = threshold_val - - # add max iterations to the payload - max_iterations = env_config.get("max_iterations") - if max_iterations is not None: - kpi_payload["max_iterations"] = max_iterations - - return kpi_payload - - -def process_kpi_data(kpi_payloads, tag, timestamp): - """Combine and augment the KPI payloads. - - Args: - kpi_payloads: Dictionary of KPI payloads for each job. - tag: Tag for the KPI payload. - timestamp: Timestamp to use (ISO format). - """ - # accumulate workflow outcomes - totals = {} - successes = {} - failures_did_not_finish = {} - failures_did_not_pass_thresholds = {} - for job_id, kpi_payload in kpi_payloads.items(): - workflow = job_id.split(":")[0] - if workflow not in totals: - totals[workflow] = 0 - successes[workflow] = 0 - failures_did_not_finish[workflow] = 0 - failures_did_not_pass_thresholds[workflow] = 0 - totals[workflow] += 1 - if kpi_payload["success"]: - successes[workflow] += 1 - else: - if kpi_payload["msg"] == "error: training did not finish!": - failures_did_not_finish[workflow] += 1 - else: - failures_did_not_pass_thresholds[workflow] += 1 - - kpi_payloads["overall"] = { - "totals": totals, - "successes": successes, - "failures_did_not_finish": failures_did_not_finish, - "failures_did_not_pass_thresholds": failures_did_not_pass_thresholds, - "timestamp": timestamp, - "tag": tag, - } - - return kpi_payloads - - -def output_payloads(payloads): - """Output the KPI payloads to a json file.""" - # first grab all log files - repo_path = _get_repo_path() - output_path = os.path.join(repo_path, "logs/kpi.json") - # create directory if it doesn't exist - if not os.path.exists(os.path.dirname(output_path)): - os.makedirs(os.path.dirname(output_path)) - # save file - with open(output_path, "w") as payload_file: - json.dump(payloads, payload_file, indent=4) - - -def _retrieve_logs(workflow, task): - """Retrieve training logs.""" - # first grab all log files - repo_path = _get_repo_path() - - # Defer Isaac Sim version import to avoid preloading USD before SimulationApp starts. - from isaaclab.utils.version import get_isaac_sim_version - - if get_isaac_sim_version().major < 5: - repo_path = os.path.join(repo_path, "..") - if workflow == "rl_games": - log_files_path = os.path.join(repo_path, f"logs/{workflow}/{task}/*/summaries/*") - elif workflow == "sb3": - log_files_path = os.path.join(repo_path, f"logs/{workflow}/{task}/*/*/*.tfevents.*") - else: - log_files_path = os.path.join(repo_path, f"logs/{workflow}/{task}/*/*.tfevents.*") - log_files = glob.glob(log_files_path) - # handle case where no log files are found - if not log_files: - return None - # find most recent - latest_log_file = max(log_files, key=os.path.getctime) - # parse tf file into a dictionary - log_data = _parse_tf_logs(latest_log_file) - - # validate that log data contains entries - if not log_data: - print(f"Warning: Log file {latest_log_file} parsed but contains no data") - return None - - return log_data - - -def _parse_tf_logs(log): - """Parse the tensorflow filepath into a dictionary.""" - # Defer tensorboard import to avoid side effects during pytest collection. - from tensorboard.backend.event_processing import event_accumulator - - log_data = {} - ea = event_accumulator.EventAccumulator(log) - ea.Reload() - tags = ea.Tags()["scalars"] - for tag in tags: - log_data[tag] = [] - for event in ea.Scalars(tag): - log_data[tag].append((event.step, event.value)) - return log_data - - -def _extract_log_val(name, log_data, uses_lower_threshold, workflow): - """Extract the value from the log data.""" - try: - if name == "reward": - reward_tags = { - "rl_games": "rewards/iter", - "rsl_rl": "Train/mean_reward", - "sb3": "rollout/ep_rew_mean", - "skrl": "Reward / Total reward (mean)", - } - tag = reward_tags.get(workflow) - if tag: - return _extract_reward(log_data, tag) - - elif name == "episode_length": - episode_tags = { - "rl_games": "episode_lengths/iter", - "rsl_rl": "Train/mean_episode_length", - "sb3": "rollout/ep_len_mean", - "skrl": "Episode / Total timesteps (mean)", - } - tag = episode_tags.get(workflow) - if tag: - return _extract_feature(log_data, tag, uses_lower_threshold) - except KeyError as e: - print(f"Warning: Metric '{name}' not found in logs for workflow '{workflow}': {e}") - return None - except Exception as e: - print(f"Error extracting '{name}' for workflow '{workflow}': {e}") - return None - - raise ValueError(f"Env Config name {name} is not supported.") - - -def _extract_feature(log_data, feature, uses_lower_threshold): - """Extract the feature from the log data.""" - log_data = np.array(log_data[feature])[:, 1] - - if uses_lower_threshold: - return max(log_data) - else: - return min(log_data) - - -def _extract_reward(log_data, feature, k=8): - """Extract the averaged max reward from the log data.""" - log_data = np.array(log_data[feature])[:, 1] - - # find avg of k max values - k = min(len(log_data), k) - averaged_reward = np.mean(np.partition(log_data, -k)[-k:]) - - return averaged_reward diff --git a/source/isaaclab_tasks/test/core/test_environment_determinism.py b/source/isaaclab_tasks/test/core/test_environment_determinism.py index f457a6b6b043..c808b8a05cb2 100644 --- a/source/isaaclab_tasks/test/core/test_environment_determinism.py +++ b/source/isaaclab_tasks/test/core/test_environment_determinism.py @@ -65,28 +65,6 @@ def test_locomotion_env_determinism(task_name, device): _test_environment_determinism(task_name, device, physics_preset_name="isaacsim_physx") -@pytest.mark.parametrize( - "task_name", - [ - pytest.param( - "Isaac-Reorient-Cube-Allegro", - marks=pytest.mark.skip( - reason=( - "Free rigid bodies are not bit-reproducible on Newton/CUDA:" - " Isaac-Lift-Franka fails the same assertion on develop. This task passed only" - " while its cube was declared as an articulation with no joints." - ) - ), - ), - # "Isaac-Reorient-Cube-Allegro-Direct", # FIXME: @kellyg, any idea why it is not deterministic? - ], -) -@pytest.mark.parametrize("device", ["cuda", "cpu"]) -def test_dextrous_env_determinism(task_name, device): - """Check deterministic environment creation for dextrous manipulation.""" - _test_environment_determinism(task_name, device) - - def test_newton_cartpole_env_determinism(): """Check deterministic stepping for a Newton environment.""" # One small CUDA-only case at a quarter of the default steps bounds Newton kernel compilation and test runtime. diff --git a/source/isaaclab_tasks/test/core/test_environments_isaacsim_physx.py b/source/isaaclab_tasks/test/core/test_environments_isaacsim_physx.py index 5361b6e7fb77..b2dbe5ffed6e 100644 --- a/source/isaaclab_tasks/test/core/test_environments_isaacsim_physx.py +++ b/source/isaaclab_tasks/test/core/test_environments_isaacsim_physx.py @@ -25,8 +25,6 @@ "Isaac-Cartpole-Camera-Direct", # Already covered by test_rendering_cartpole.py "Isaac-Lift-Cloth-Franka-Camera", # Already covered by test_rendering_franka_cloth.py "Isaac-Lift-KukaAllegro-Camera", # Already covered by test_rendering_lift_kuka_hetero.py - "Isaac-Lift-Soft-Franka", # Temporarily excluded because it can crash the test process - "Isaac-Lift-Soft-Franka-Camera", # Temporarily excluded because it can crash the test process "Isaac-Reorient-Cube-Shadow-Camera-Direct", # Already covered by test_rendering_shadow_hand.py "Isaac-Velocity-Flat-AnymalD", # Already covered by test_environment_determinism.py "Isaac-Velocity-Rough-AnymalD", # Already covered by test_environment_determinism.py @@ -40,14 +38,10 @@ ) -@pytest.mark.parametrize( - "task_name", - _ENVIRONMENT_TASKS, -) -@pytest.mark.parametrize("num_envs, device", [(2, "cuda")]) +@pytest.mark.parametrize("task_name", _ENVIRONMENT_TASKS) @pytest.mark.isaacsim_ci -def test_environments_isaacsim_physx(task_name, num_envs, device): - _run_environments(task_name, device, num_envs, physics_preset_name="isaacsim_physx") +def test_environments_isaacsim_physx(task_name): + _run_environments(task_name, "cuda", 2, physics_preset_name="isaacsim_physx") @pytest.mark.parametrize("task_name", [task for task in _ENVIRONMENT_TASKS if task in SINGLE_ENVIRONMENT_TASKS]) diff --git a/source/isaaclab_tasks/test/core/test_environments_newton.py b/source/isaaclab_tasks/test/core/test_environments_newton.py index 2830f27a09f8..00dfdab3f988 100644 --- a/source/isaaclab_tasks/test/core/test_environments_newton.py +++ b/source/isaaclab_tasks/test/core/test_environments_newton.py @@ -35,13 +35,9 @@ ) -@pytest.mark.parametrize( - "task_name", - _ENVIRONMENT_TASKS, -) -@pytest.mark.parametrize("num_envs, device", [(2, "cuda")]) -def test_environments_newton(task_name, num_envs, device): - _run_environments(task_name, device, num_envs, physics_preset_name="newton_mjwarp") +@pytest.mark.parametrize("task_name", _ENVIRONMENT_TASKS) +def test_environments_newton(task_name): + _run_environments(task_name, "cuda", 2, physics_preset_name="newton_mjwarp") @pytest.mark.parametrize("task_name", [task for task in _ENVIRONMENT_TASKS if task in SINGLE_ENVIRONMENT_TASKS]) diff --git a/source/isaaclab_tasks/test/core/test_environments_ovphysx.py b/source/isaaclab_tasks/test/core/test_environments_ovphysx.py index 9fe12c4b9b2c..eb2f2b03ba33 100644 --- a/source/isaaclab_tasks/test/core/test_environments_ovphysx.py +++ b/source/isaaclab_tasks/test/core/test_environments_ovphysx.py @@ -36,13 +36,9 @@ ) -@pytest.mark.parametrize( - "task_name", - _ENVIRONMENT_TASKS, -) -@pytest.mark.parametrize("num_envs, device", [(2, "cuda")]) -def test_environments_ovphysx(task_name, num_envs, device): - _run_environments(task_name, device, num_envs, physics_preset_name="ovphysx") +@pytest.mark.parametrize("task_name", _ENVIRONMENT_TASKS) +def test_environments_ovphysx(task_name): + _run_environments(task_name, "cuda", 2, physics_preset_name="ovphysx") @pytest.mark.parametrize("task_name", [task for task in _ENVIRONMENT_TASKS if task in SINGLE_ENVIRONMENT_TASKS]) diff --git a/source/isaaclab_tasks/test/core/test_lazy_export_stubs.py b/source/isaaclab_tasks/test/core/test_lazy_export_stubs.py index b7e9a824d727..0c23d79e30f9 100644 --- a/source/isaaclab_tasks/test/core/test_lazy_export_stubs.py +++ b/source/isaaclab_tasks/test/core/test_lazy_export_stubs.py @@ -18,15 +18,17 @@ import tempfile from pathlib import Path -import pytest - from isaaclab.utils.module import _parse_stub _SOURCE_ROOT = Path(__file__).resolve().parent.parent.parent -def _find_lazy_export_calls() -> list[tuple[Path, int, str]]: - """Return ``(file, lineno, source_line)`` for every ``lazy_export(...)`` with args.""" +def _find_lazy_export_calls() -> tuple[int, list[tuple[Path, int, str]]]: + """Return the number of ``lazy_export`` call sites and every call that passes arguments. + + Each violation is reported as ``(file, lineno, source_line)``. + """ + num_calls = 0 results: list[tuple[Path, int, str]] = [] for root, _dirs, files in os.walk(_SOURCE_ROOT): for fname in files: @@ -50,39 +52,23 @@ def _find_lazy_export_calls() -> list[tuple[Path, int, str]]: ) if not is_lazy_export: continue + num_calls += 1 if node.args or node.keywords: line = source.splitlines()[node.lineno - 1].strip() results.append((path, node.lineno, line)) - return sorted(results) - - -_VIOLATIONS = _find_lazy_export_calls() -_IDS = [f"{p.relative_to(_SOURCE_ROOT)}:{lineno}" for p, lineno, _ in _VIOLATIONS] - - -@pytest.mark.parametrize("violation", _VIOLATIONS or [None], ids=_IDS or ["no-violations"]) -def test_lazy_export_has_no_args(violation: tuple[Path, int, str] | None): - """lazy_export() must be called with no arguments.""" - if violation is None: - return - path, lineno, line = violation - pytest.fail( - f"{path.relative_to(_SOURCE_ROOT)}:{lineno}: {line}\n\n" - "lazy_export() should take no arguments. Move fallback packages into\n" - "the .pyi stub as 'from import *' and remove the packages= arg." - ) + return num_calls, sorted(results) -def test_no_lazy_export_violations_found(): - """Canary: confirm we actually scanned files (guard against broken discovery).""" - init_count = sum( - 1 - for root, _dirs, files in os.walk(_SOURCE_ROOT) - for f in files - if f == "__init__.py" and "lazy_export" in (Path(root) / f).read_text(errors="ignore") +def test_lazy_export_has_no_args(): + """Every ``lazy_export()`` call must pass no arguments; fallback packages belong in the ``.pyi`` stub.""" + num_calls, violations = _find_lazy_export_calls() + assert num_calls > 0, "No lazy_export() call sites found; the discovery may be broken." + formatted = "\n".join(f"{path.relative_to(_SOURCE_ROOT)}:{lineno}: {line}" for path, lineno, line in violations) + assert not violations, ( + f"{formatted}\n\nlazy_export() should take no arguments. Move fallback packages into the .pyi stub as" + " 'from import *' and remove the packages= argument." ) - assert init_count > 0, "No __init__.py files with lazy_export() found — discovery may be broken" # --------------------------------------------------------------------------- diff --git a/source/isaaclab_tasks/test/core/test_lift_env_cfg.py b/source/isaaclab_tasks/test/core/test_lift_env_cfg.py index eb6de255122b..13731340cfd6 100644 --- a/source/isaaclab_tasks/test/core/test_lift_env_cfg.py +++ b/source/isaaclab_tasks/test/core/test_lift_env_cfg.py @@ -18,6 +18,7 @@ from isaaclab_tasks.core.lift import mdp from isaaclab_tasks.core.lift.config.franka.franka_env_cfg import FrankaLiftEnvCfg, FrankaReorientEnvCfg from isaaclab_tasks.core.lift.config.franka_soft.franka_soft_env_cfg import FrankaSoftEnvCfg +from isaaclab_tasks.core.lift.mdp.commands import pose_commands from isaaclab_tasks.core.lift.mdp.commands.pose_commands import ( CableUniformPoseCommand, DeformableUniformPoseCommand, @@ -85,13 +86,32 @@ def test_franka_rigid_tasks_select_collision_meshes_for_reset_clearance(cfg_type assert not stage.GetPrimAtPath("/Robot/link1_capsule").IsValid() +def _make_vision_camera(data_type: str, images: torch.Tensor) -> mdp.vision_camera: + """Build a ``vision_camera`` term around a fake single-data-type camera sensor.""" + sensor = SimpleNamespace( + cfg=SimpleNamespace(data_types=[data_type]), data=SimpleNamespace(output={data_type: images}) + ) + term = object.__new__(mdp.vision_camera) + term.sensor = sensor + term.sensor_type = data_type + term._is_depth = data_type in ("distance_to_image_plane", "depth") + return term + + def test_camera_normalization_is_stationary() -> None: - """RGB and depth normalization must not depend on per-frame statistics.""" - rgb = torch.tensor([0.0, 127.5, 255.0]) - depth = torch.tensor([0.0, 2.0]) + """RGB and depth normalization must map fixed inputs to fixed outputs, independent of per-frame statistics.""" + rgb = torch.tensor([0.0, 127.5, 255.0]).view(1, 1, 1, 3) + depth = torch.tensor([0.0, 2.0]).view(1, 1, 2, 1) + env = SimpleNamespace() + + rgb_obs = _make_vision_camera("rgb", rgb)(env, sensor_cfg=None) + depth_obs = _make_vision_camera("depth", depth)(env, sensor_cfg=None) - assert torch.allclose(mdp.vision_camera._rgb_norm(None, rgb), torch.tensor([-0.5, 0.0, 0.5])) - assert torch.allclose(mdp.vision_camera._depth_norm(None, depth), torch.tanh(depth / 2) - 0.5) + # channel-first output with the value range mapped to [-0.5, 0.5) + assert rgb_obs.shape == (1, 3, 1, 1) + assert torch.allclose(rgb_obs.flatten(), torch.tensor([-0.5, 0.0, 0.5])) + assert depth_obs.shape == (1, 1, 1, 2) + assert torch.allclose(depth_obs.flatten(), torch.tanh(torch.tensor([0.0, 2.0]) / 2) - 0.5) def test_lift_pose_markers_forward_environment_ids(monkeypatch: pytest.MonkeyPatch) -> None: @@ -99,7 +119,7 @@ def test_lift_pose_markers_forward_environment_ids(monkeypatch: pytest.MonkeyPat num_envs = 3 environment_ids = torch.arange(num_envs) identity_quat = torch.zeros((num_envs, 4)) - identity_quat[:, 0] = 1.0 + identity_quat[:, 3] = 1.0 root_pos_w = torch.zeros((num_envs, 3)) root_pose_w = torch.cat((root_pos_w, identity_quat), dim=-1) @@ -138,7 +158,7 @@ def _initialize_command_term(command, command_cfg, command_env) -> None: command.metrics = {} monkeypatch.setattr(CommandTerm, "__init__", _initialize_command_term) - monkeypatch.setattr("isaaclab.markers.VisualizationMarkers", _MarkerSpy) + monkeypatch.setattr(pose_commands, "VisualizationMarkers", _MarkerSpy) command = ObjectUniformPoseCommand(cfg, env) command._set_debug_vis_impl(True) @@ -167,7 +187,7 @@ def test_lift_point_cloud_markers_repeat_environment_ids_per_point() -> None: num_envs = 3 num_points = 4 identity_quat = torch.zeros((num_envs, 4)) - identity_quat[:, 0] = 1.0 + identity_quat[:, 3] = 1.0 root_pos_w = torch.zeros((num_envs, 3)) points_local = torch.arange(num_envs * num_points * 3, dtype=torch.float32).view(num_envs, num_points, 3) @@ -187,10 +207,13 @@ def test_lift_point_cloud_markers_repeat_environment_ids_per_point() -> None: term.points_local = points_local term.points_w = torch.zeros_like(points_local) term.visualizer = _MarkerSpy() + term._marker_env_ids = torch.arange(num_envs).repeat_interleave(num_points) env = SimpleNamespace(num_envs=num_envs) - term(env, num_points=num_points, visualize=True) + points_b = term(env, num_points=num_points, visualize=True) + # identity poses: the points in the reference frame are the local points + assert torch.allclose(points_b, points_local) assert len(term.visualizer.calls) == 1 _, kwargs = term.visualizer.calls[0] assert torch.equal(kwargs["translations"], term.points_w.view(-1, 3)) diff --git a/source/isaaclab_tasks/test/core/test_multi_agent_environments.py b/source/isaaclab_tasks/test/core/test_multi_agent_environments.py index d43a81222eec..23f318f25239 100644 --- a/source/isaaclab_tasks/test/core/test_multi_agent_environments.py +++ b/source/isaaclab_tasks/test/core/test_multi_agent_environments.py @@ -25,10 +25,5 @@ @pytest.mark.parametrize("num_envs, device", [(2, "cuda"), (1, "cuda")]) @pytest.mark.parametrize("task_name", setup_environment(multi_agent=True, tier="core")) def test_environments(task_name, num_envs, device): - """Run all environments with given parameters and check environments return valid signals.""" - print(f">>> Running test for environment: {task_name} with num_envs={num_envs} and device={device}") - # check environment + """Run all multi-agent environments with random actions and check that they return valid signals.""" _check_random_actions(task_name, device, num_envs, multi_agent=True) - # close the environment - print(f">>> Closing environment: {task_name}") - print("-" * 80) diff --git a/source/isaaclab_tasks/test/core/test_newton_actuator_manager_authoring.py b/source/isaaclab_tasks/test/core/test_newton_actuator_manager_authoring.py index 87504c62e799..64d53e3345e7 100644 --- a/source/isaaclab_tasks/test/core/test_newton_actuator_manager_authoring.py +++ b/source/isaaclab_tasks/test/core/test_newton_actuator_manager_authoring.py @@ -15,8 +15,6 @@ simulation_app = AppLauncher(headless=True).app -import unittest # noqa: E402 - from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg # noqa: E402 from isaaclab_newton.physics import NewtonManager as SimulationManager # noqa: E402 @@ -40,47 +38,32 @@ ) -class TestManagerBasedSceneNewtonActuatorAuthoring(unittest.TestCase): - """Regression test for Newton actuator authoring in manager-based clone paths.""" - - def test_newton_actuators_present_for_g1_manager_env(self): - env_cfg = G1FlatEnvCfg() - env_cfg.scene.num_envs = 1 - env_cfg.decimation = 1 - env_cfg.scene.contact_forces = None - env_cfg.rewards.feet_air_time = None - env_cfg.rewards.feet_slide = None - env_cfg.terminations.base_contact = None - env_cfg.sim = SimulationCfg(physics=_NEWTON_CFG, use_newton_actuators=True) - # A single explicit group covering all joints is enough to exercise the - # NewtonActuator authoring clone path; the group count is not the point. - env_cfg.scene.robot.actuators = { - "all": DCMotorCfg( - joint_names_expr=[".*"], - saturation_effort=300.0, - actuator_effort_limit=300.0, - actuator_velocity_limit=20.0, - stiffness=150.0, - damping=5.0, - ), - } - env = ManagerBasedRLEnv(cfg=env_cfg) - try: - stage = env.unwrapped.sim.stage - actuator_prim_count = sum(1 for prim in stage.Traverse() if prim.GetTypeName() == "NewtonActuator") - self.assertGreater( - actuator_prim_count, - 0, - "Expected authored NewtonActuator prims in manager-based scene workflow.", - ) - self.assertGreater( - len(SimulationManager.get_model().actuators), - 0, - "Expected Newton model actuators to be non-empty with use_newton_actuators=True.", - ) - finally: - env.close() - - -if __name__ == "__main__": - unittest.main() +def test_newton_actuators_present_for_g1_manager_env(): + """Explicit actuator groups must author NewtonActuator prims and populate the Newton model.""" + env_cfg = G1FlatEnvCfg() + env_cfg.scene.num_envs = 1 + env_cfg.decimation = 1 + env_cfg.scene.contact_forces = None + env_cfg.rewards.feet_air_time = None + env_cfg.rewards.feet_slide = None + env_cfg.terminations.base_contact = None + env_cfg.sim = SimulationCfg(physics=_NEWTON_CFG, use_newton_actuators=True) + # a single explicit group covering all joints is enough to exercise the authoring clone path + env_cfg.scene.robot.actuators = { + "all": DCMotorCfg( + joint_names_expr=[".*"], + saturation_effort=300.0, + actuator_effort_limit=300.0, + actuator_velocity_limit=20.0, + stiffness=150.0, + damping=5.0, + ), + } + env = ManagerBasedRLEnv(cfg=env_cfg) + try: + stage = env.unwrapped.sim.stage + actuator_prim_count = sum(1 for prim in stage.Traverse() if prim.GetTypeName() == "NewtonActuator") + assert actuator_prim_count > 0, "Expected authored NewtonActuator prims in the manager-based scene." + assert len(SimulationManager.get_model().actuators) > 0, "Expected Newton model actuators to be non-empty." + finally: + env.close() diff --git a/source/isaaclab_tasks/test/core/test_video_recording.py b/source/isaaclab_tasks/test/core/test_video_recording.py index 7b858574bed9..c564bca8fe03 100644 --- a/source/isaaclab_tasks/test/core/test_video_recording.py +++ b/source/isaaclab_tasks/test/core/test_video_recording.py @@ -5,25 +5,8 @@ """End-to-end video recording tests covering all VideoRecorder sources. -Each test writes real mp4 clips and reads them back to verify content. - -Sources tested: - "visualizer:kit" – Kit Replicator viewport (PhysX) - "visualizer:kit"+Newton – logs error, no clip written - "visualizer:newton" – Newton GL framebuffer (Newton physics) - "sensor:tiled_camera" – tiled camera sensor (PhysX, RTX renderer) - multiple recorders – Kit viewport + sensor written simultaneously - -Setup: - - AppLauncher(headless=True, enable_cameras=True) - - CartpoleEnv or CartpoleCameraEnv, stepped for _STEPS env steps per test. - - VideoRecorderCfg(video_length=_CLIP, video_interval=0) → one clip per test. -Tests: - - kit_physx → non-black ✓, motion ✓ - - kit_newton → error logged ✓, no clip ✓ - - newton → non-black ✓ (motion skipped — Newton GL renders asynchronously) - - sensor_physx → non-black ✓, motion ✓ - - multi_recorder → kit clip ✓, sensor clip ✓, both non-black and moving +Each test steps a cartpole environment with one or more recorders attached, writes real mp4 clips and +reads them back to verify that they are non-black and, for synchronous render sources, show motion. """ # Check for moviepy before launching Kit so a missing dependency produces a diff --git a/source/isaaclab_tasks/test/env_test_utils.py b/source/isaaclab_tasks/test/env_test_utils.py index fa6eefcb8882..d72a998ffb8e 100644 --- a/source/isaaclab_tasks/test/env_test_utils.py +++ b/source/isaaclab_tasks/test/env_test_utils.py @@ -23,13 +23,8 @@ from isaaclab_tasks.utils.hydra import collect_presets, resolve_presets from isaaclab_tasks.utils.parse_cfg import load_cfg_from_registry, parse_env_cfg -# Map of task IDs to the reason for marking the corresponding parametrized -# test cases as expected failures. Tests that consume :func:`setup_environment` -# automatically pick up these marks via :class:`pytest.param`. -XFAIL_TASKS: dict[str, str] = {} - -# Native crashes cannot be contained by xfail because the process exits before -# pytest records an outcome. Temporarily skip these tasks in every environment smoke suite. +# Native crashes cannot be contained by xfail because the process exits before pytest records an +# outcome, so these tasks are skipped in every environment smoke suite. SKIP_TASKS: dict[str, str] = { "Isaac-Lift-Soft-Franka": "Temporarily skipped because the soft-lift environment can crash the test process.", "Isaac-Lift-Soft-Franka-Camera": ( @@ -132,18 +127,10 @@ def setup_environment( print(">>> All registered environments:", registered_tasks) - # Apply skip before xfail so native-crash exclusions never execute. - marked_tasks = [] - for task_id in registered_tasks: - if task_id in SKIP_TASKS: - marked_tasks.append(pytest.param(task_id, marks=pytest.mark.skip(reason=SKIP_TASKS[task_id]))) - elif task_id in XFAIL_TASKS: - marked_tasks.append( - pytest.param(task_id, marks=pytest.mark.xfail(reason=XFAIL_TASKS[task_id], strict=False)) - ) - else: - marked_tasks.append(task_id) - return marked_tasks + return [ + pytest.param(task_id, marks=pytest.mark.skip(reason=SKIP_TASKS[task_id])) if task_id in SKIP_TASKS else task_id + for task_id in registered_tasks + ] def _fire_all_interval_events_once(env) -> None: @@ -283,7 +270,6 @@ def _run_environments( if "Visuomotor" in task_name and num_envs == 32: return - print(f""">>> Running test for environment: {task_name}""") _check_random_actions( task_name, device, @@ -294,8 +280,6 @@ def _run_environments( disable_clone_in_fabric=disable_clone_in_fabric, physics_preset_name=physics_preset_name, ) - print(f""">>> Closing environment: {task_name}""") - print("-" * 80) def _check_random_actions( diff --git a/source/isaaclab_tasks/test/test_maybe_save_stage_golden.py b/source/isaaclab_tasks/test/test_maybe_save_stage_golden.py index fd44edb6ff8a..39764f287e48 100644 --- a/source/isaaclab_tasks/test/test_maybe_save_stage_golden.py +++ b/source/isaaclab_tasks/test/test_maybe_save_stage_golden.py @@ -7,13 +7,11 @@ from __future__ import annotations -import os from pathlib import Path from unittest import mock import pytest from rendering_test_utils import ( - _GOLDEN_STAGES_DIRECTORY, compare_golden_stage, maybe_save_stage, ) @@ -194,11 +192,6 @@ def test_maybe_save_stage_noop_without_dump_or_compare(monkeypatch: pytest.Monke save_stage_mock.assert_not_called() -def test_golden_stages_directory_exists_in_repo(): - """The checked-in golden stage directory is present for LFS baselines.""" - assert os.path.isdir(_GOLDEN_STAGES_DIRECTORY) - - # --------------------------------------------------------------------------- # ISAAC_LAB_SAVE_STAGES path # --------------------------------------------------------------------------- diff --git a/source/isaaclab_tasks_experimental/changelog.d/unify-task-coding-style.skip b/source/isaaclab_tasks_experimental/changelog.d/unify-task-coding-style.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab_tasks_experimental/isaaclab_tasks_experimental/core/cartpole/mdp/rewards.py b/source/isaaclab_tasks_experimental/isaaclab_tasks_experimental/core/cartpole/mdp/rewards.py index f970a4789dfe..0e7ac5ca0fcf 100644 --- a/source/isaaclab_tasks_experimental/isaaclab_tasks_experimental/core/cartpole/mdp/rewards.py +++ b/source/isaaclab_tasks_experimental/isaaclab_tasks_experimental/core/cartpole/mdp/rewards.py @@ -78,7 +78,7 @@ def _survival_rate_kernel( class survival_success_rate(ManagerTermBase): """Tracks episode survival as the success metric (Warp-first). - Twin of :class:`isaaclab_tasks.core.cartpole.mdp.rewards.survival_success_rate`. + Twin of :class:`isaaclab.envs.mdp.rewards.survival_success_rate`. Returns zero reward (pure metric tracking). On reset, computes the fraction of just-reset environments that timed out (survived the full episode) entirely on-device and exposes it as ``Metrics/success_rate`` through the reward diff --git a/source/isaaclab_tasks_experimental/isaaclab_tasks_experimental/core/locomotion/mdp/rewards.py b/source/isaaclab_tasks_experimental/isaaclab_tasks_experimental/core/locomotion/mdp/rewards.py index e180774bb9b7..17ac52d27ca1 100644 --- a/source/isaaclab_tasks_experimental/isaaclab_tasks_experimental/core/locomotion/mdp/rewards.py +++ b/source/isaaclab_tasks_experimental/isaaclab_tasks_experimental/core/locomotion/mdp/rewards.py @@ -346,7 +346,7 @@ def _terminated_penalty_kernel( def terminated_penalty(env: ManagerBasedRLEnv, out: wp.array(dtype=wp.float32)) -> None: """One-off penalty for terminating early, independent of the environment step size. - Warp-first override of :func:`isaaclab_tasks.core.locomotion.mdp.rewards.terminated_penalty`. + Warp-first override of :func:`isaaclab.envs.mdp.rewards.terminated_penalty`. """ wp.launch( kernel=_terminated_penalty_kernel, @@ -392,7 +392,7 @@ def _survival_rate_kernel( class survival_success_rate(ManagerTermBase): """Tracks episode survival as the success metric (Warp-first). - Twin of :class:`isaaclab_tasks.core.locomotion.mdp.rewards.survival_success_rate`. + Twin of :class:`isaaclab.envs.mdp.rewards.survival_success_rate`. Returns zero reward (pure metric tracking). On reset, computes the fraction of just-reset environments that timed out (survived the full episode) entirely on-device and exposes it as ``Metrics/success_rate`` through the reward manager's reset extras. From 5be871635041678e7c40d3f82bff561a4863b880 Mon Sep 17 00:00:00 2001 From: Mustafa Haiderbhai Date: Sun, 20 Sep 2026 09:32:37 -0700 Subject: [PATCH 2/2] Address review feedback on the isaaclab_tasks cleanup pass - Use relative imports within task subpackages (max three dots) and absolute imports across packages - Own the Humanoid terrain in humanoid_common instead of importing it from Ant - Re-export the moved shared MDP terms from the task stubs so wildcard imports keep working - Drop the leading underscore from helpers imported across files - Clarify the power_consumption docstring and other readability nits - Fix the keyboard SuccessMonitor import and the capture-safety test lookups --- .../isaaclab/isaaclab/envs/mdp/curriculums.py | 2 ++ source/isaaclab/isaaclab/envs/mdp/rewards.py | 3 ++- .../test/envs/mdp/test_capture_safety.py | 8 +++---- .../changelog.d/unify-task-coding-style.rst | 12 ++++++++-- .../contrib/assemble_trocar/mdp/rewards.py | 2 +- .../contrib/automate/assembly_env.py | 2 +- .../contrib/automate/disassembly_env.py | 2 +- .../contrib/automate/factory_control.py | 2 +- .../contrib/deploy/mdp/events.py | 2 +- .../contrib/dr_legs/mdp/__init__.pyi | 2 ++ .../contrib/franka_pour/mdp/observations.py | 4 ++-- .../contrib/franka_pour/mdp/reset_dataset.py | 4 ++-- .../contrib/franka_pour/mdp/rewards.py | 4 ++-- .../contrib/franka_pour/pour_env.py | 4 ++-- .../contrib/franka_pour/pour_env_cfg.py | 2 +- .../contrib/franka_pour/reset_sampler.py | 2 +- .../keyboard/keyboards/keyboard_labels.py | 6 ++--- .../keyboard/mdp/commands/typing_commands.py | 3 +-- .../keyboard/mdp/commands/typing_vis.py | 8 +++---- .../mdp/observations.py | 6 ++--- .../multitask_manipulation/mdp/rewards.py | 12 +++++----- .../multitask_manipulation/mdp/utils.py | 2 +- .../isaaclab_tasks/contrib/nist/mdp/events.py | 6 ++--- .../exhaustpipe_gr1t2_pink_ik_env_cfg.py | 4 ++-- .../nutpour_gr1t2_pink_ik_env_cfg.py | 4 ++-- .../pick_place/pickplace_gr1t2_env_cfg.py | 4 ++-- .../pickplace_gr1t2_waist_enabled_env_cfg.py | 4 ++-- .../contrib/stack/mdp/stack_events.py | 6 ++--- .../core/cabinet/cabinet_direct_env.py | 4 ++-- .../core/cabinet/cabinet_direct_env_cfg.py | 2 +- .../core/cabinet/cabinet_env_cfg.py | 3 ++- .../config/franka/cabinet_direct_env_cfg.py | 4 ++-- .../config/franka/joint_pos_env_cfg.py | 6 ++--- .../core/cartpole/agents/rsl_rl_ppo_cfg.py | 3 ++- .../core/cartpole/cartpole_common.py | 2 +- .../cartpole/cartpole_direct_camera_env.py | 7 +++--- .../cartpole_direct_camera_env_cfg.py | 6 +++-- .../core/cartpole/cartpole_direct_env.py | 2 +- .../core/cartpole/cartpole_direct_env_cfg.py | 4 ++-- .../cartpole_manager_camera_env_cfg.py | 7 +++--- .../core/cartpole/cartpole_manager_env_cfg.py | 6 ++--- .../core/cartpole/mdp/__init__.pyi | 2 ++ .../fourbar_pole_manager_env_cfg.py | 3 ++- .../core/handover/handover_env.py | 16 ++++++------- .../core/handover/handover_env_cfg.py | 3 ++- .../core/handover/handover_manager_env_cfg.py | 11 +++++---- .../core/handover/mdp/commands_cfg.py | 3 +-- .../core/lift/adr_curriculum.py | 2 +- .../core/lift/config/franka/franka_env_cfg.py | 6 ++--- .../franka_soft/franka_cable_env_cfg.py | 5 ++-- .../franka_soft/franka_cloth_env_cfg.py | 5 ++-- .../config/franka_soft/franka_soft_env_cfg.py | 3 ++- .../lift/config/kuka_allegro/camera_cfg.py | 5 ++-- .../kuka_allegro_camera_env_cfg.py | 7 +++--- .../kuka_allegro/kuka_allegro_env_cfg.py | 8 +++---- .../isaaclab_tasks/core/lift/lift_env_cfg.py | 7 ++++-- .../isaaclab_tasks/core/lift/mdp/__init__.pyi | 2 ++ .../core/lift/mdp/commands/pose_commands.py | 3 ++- .../core/lift/mdp/observations.py | 10 +++++--- .../isaaclab_tasks/core/lift/mdp/rewards.py | 3 +-- .../isaaclab_tasks/core/lift/mdp/utils.py | 18 +++++++-------- .../core/locomotion/ant/ant_direct_env.py | 4 ++-- .../core/locomotion/ant/ant_direct_env_cfg.py | 6 ++--- .../locomotion/ant/ant_manager_env_cfg.py | 8 +++---- .../locomotion/humanoid/humanoid_common.py | 17 ++++++++++++++ .../humanoid/humanoid_direct_env.py | 4 ++-- .../humanoid/humanoid_direct_env_cfg.py | 10 ++------ .../humanoid/humanoid_manager_env_cfg.py | 8 +++---- .../core/locomotion/locomotion_direct_env.py | 4 ++-- .../core/locomotion/mdp/__init__.pyi | 2 ++ .../core/locomotion/mdp/rewards.py | 14 +++++++---- .../core/pendulum/pendulum_marl_env.py | 2 +- .../config/franka/franka_reach_env_cfg.py | 3 ++- .../config/franka/franka_reach_osc_env_cfg.py | 3 ++- .../reach/config/ur_10/joint_pos_env_cfg.py | 4 ++-- .../allegro_hand_direct_env_cfg.py | 6 ++--- .../allegro_hand_manager_env_cfg.py | 13 ++++++----- .../shadow_hand_camera_manager_env_cfg.py | 17 +++++++------- .../config/shadow_hand/shadow_hand_common.py | 3 ++- .../shadow_hand_direct_camera_env.py | 8 +++---- .../shadow_hand_direct_camera_env_cfg.py | 9 ++++---- .../shadow_hand/shadow_hand_direct_env.py | 6 ++--- .../shadow_hand/shadow_hand_direct_env_cfg.py | 6 ++--- .../shadow_hand_manager_env_cfg.py | 23 ++++++++++--------- .../core/reorient/mdp/commands.py | 2 +- .../core/reorient/mdp/events.py | 2 +- .../core/reorient/mdp/observations.py | 8 +++---- .../core/reorient/reorient_direct_env.py | 16 ++++++------- .../core/reorient/reorient_manager_env_cfg.py | 3 ++- .../velocity/config/anymal_d/rough_env_cfg.py | 4 ++-- .../velocity/config/cassie/rough_env_cfg.py | 8 +++---- .../core/velocity/config/g1/rough_env_cfg.py | 8 +++---- .../core/velocity/config/go2/rough_env_cfg.py | 4 ++-- .../core/velocity/config/h1/rough_env_cfg.py | 8 +++---- .../core/velocity/velocity_env_cfg.py | 3 ++- .../isaaclab_tasks/utils/hydra.py | 6 ++--- .../isaaclab_tasks/utils/parse_cfg.py | 4 ++-- .../test/contrib/test_franka_pour_env_cfg.py | 12 +++++----- .../contrib/test_franka_pour_reset_sampler.py | 8 +++---- source/isaaclab_tasks/test/core/test_hydra.py | 2 +- 100 files changed, 317 insertions(+), 261 deletions(-) diff --git a/source/isaaclab/isaaclab/envs/mdp/curriculums.py b/source/isaaclab/isaaclab/envs/mdp/curriculums.py index 39694fe38d14..910fc6dd2202 100644 --- a/source/isaaclab/isaaclab/envs/mdp/curriculums.py +++ b/source/isaaclab/isaaclab/envs/mdp/curriculums.py @@ -331,6 +331,7 @@ def __call__( promotion_only: bool = False, success_term_name: str = "success", ) -> float: + # the success term must be a class-based reward exposing a per-environment boolean ``succeeded`` buffer succeeded = env.reward_manager.get_term_cfg(success_term_name).func.succeeded[env_ids] current = self.current_difficulties[env_ids] demoted = current if promotion_only else current - 1 @@ -368,6 +369,7 @@ def initial_final_interpolate_fn( """ difficulty_term: DifficultyScheduler = getattr(env.curriculum_manager.cfg, difficulty_term_str).func frac = difficulty_term.difficulty_frac + # leave the parameter at its configured value until the curriculum has made some progress if frac < 0.1: return modify_env_param.NO_CHANGE return _interpolate_nested(initial_value, final_value, data, frac) diff --git a/source/isaaclab/isaaclab/envs/mdp/rewards.py b/source/isaaclab/isaaclab/envs/mdp/rewards.py index 3f871380832d..2526e3cfffcb 100644 --- a/source/isaaclab/isaaclab/envs/mdp/rewards.py +++ b/source/isaaclab/isaaclab/envs/mdp/rewards.py @@ -75,7 +75,8 @@ def terminated_penalty(env: ManagerBasedRLEnv) -> torch.Tensor: :class:`~isaaclab.managers.RewardManager` scales every term by the step interval, which would make a plain terminal penalty depend on ``sim.dt`` and ``decimation``. Dividing by the step interval here - cancels that scaling, so the term contributes exactly its weight on the step the episode terminates. + cancels that scaling, so the term contributes exactly its weight on the step the episode terminates. This + keeps the penalty equal to the fixed death cost the direct workflow applies. """ return env.termination_manager.terminated.float() / env.step_dt diff --git a/source/isaaclab_experimental/test/envs/mdp/test_capture_safety.py b/source/isaaclab_experimental/test/envs/mdp/test_capture_safety.py index c66f2e913a07..686c56c39aeb 100644 --- a/source/isaaclab_experimental/test/envs/mdp/test_capture_safety.py +++ b/source/isaaclab_experimental/test/envs/mdp/test_capture_safety.py @@ -63,8 +63,6 @@ import isaaclab.envs.mdp.terminations as stable_term from isaaclab.managers.manager_term_cfg import RewardTermCfg, TerminationTermCfg -import isaaclab_tasks.core.locomotion.mdp.rewards as stable_loco_rew - @dataclasses.dataclass(frozen=True) class CaptureCase: @@ -208,7 +206,7 @@ def mutate() -> None: return CaptureCase( warp_fn=warp_loco_rew.terminated_penalty, - stable_fn=stable_loco_rew.terminated_penalty, + stable_fn=stable_rew.terminated_penalty, warp_env=env, stable_env=env, params={}, @@ -228,8 +226,8 @@ def mutate() -> None: return CaptureCase( warp_fn=warp_loco_rew.survival_success_rate(cfg, env), - stable_fn=stable_loco_rew.survival_success_rate( - RewardTermCfg(func=stable_loco_rew.survival_success_rate, weight=0.0, params={}), env + stable_fn=stable_rew.survival_success_rate( + RewardTermCfg(func=stable_rew.survival_success_rate, weight=0.0, params={}), env ), warp_env=env, stable_env=env, diff --git a/source/isaaclab_tasks/changelog.d/unify-task-coding-style.rst b/source/isaaclab_tasks/changelog.d/unify-task-coding-style.rst index 2c664e7f76e6..adfd93c67690 100644 --- a/source/isaaclab_tasks/changelog.d/unify-task-coding-style.rst +++ b/source/isaaclab_tasks/changelog.d/unify-task-coding-style.rst @@ -5,8 +5,14 @@ Changed registration layout, stub (``.pyi``) layout and ``ManagerTermBase`` constructor signatures now follow one convention. * Moved the duplicated ``survival_success_rate``, ``terminated_penalty``, ``joint_pos_target_l2``, ``DifficultyScheduler`` and ``initial_final_interpolate_fn`` terms to :mod:`isaaclab.envs.mdp`. The task ``mdp`` - packages keep exposing them through their :mod:`isaaclab.envs.mdp` fallback, so ``mdp.`` references in task - configurations keep working. Import them from :mod:`isaaclab.envs.mdp` instead of the task packages. + packages re-export them, so ``mdp.`` references and wildcard imports keep working. Import them from + :mod:`isaaclab.envs.mdp` instead of the task packages. +* Unified the import style of the core task packages: relative imports within a task package, absolute imports + across packages. +* Renamed the helpers shared across modules that carried a leading underscore: ``ResetDatasetSampler`` and + ``configure_mpm_capacities`` (Franka pour), ``nearest_grasp_to_tcp_quat`` (Franka pour), ``build_gr1t2_pickplace_pipeline`` + (pick-place), ``offset_body_pose`` (multitask manipulation), ``FONT_5X7`` (keyboard), ``get_delta_dof_pos`` (factory and + AutoMate control) and :func:`isaaclab_tasks.utils.hydra.user_stacklevel`. Drop the underscore at the call sites. * Moved the fourbar-pole ``joint_pos_cos`` and ``joint_pos_sin`` observation terms from ``mdp/rewards.py`` to ``mdp/observations.py``; they remain available as ``mdp.joint_pos_cos`` and ``mdp.joint_pos_sin``. * Shared the physics, camera and asset presets of the direct and manager-based cartpole, Ant and Humanoid tasks @@ -23,6 +29,8 @@ Fixed * Fixed the lift ADR curriculum interpolating the point-cloud noise upper bound towards ``-0.01`` instead of ``0.01``. * Fixed the ``LiftEnvCfg`` configuration class missing the ``@configclass`` decorator. +* Fixed the keyboard typing command importing ``SuccessMonitor`` from the lift package instead of + :mod:`isaaclab_tasks.utils.success_monitor`. * Fixed the in-hand reorientation keypoint helpers rebuilding constant corner offsets on the device every step, and the lift deformable and cable out-of-bounds terminations allocating constant bound tensors every step. * Fixed the lift, handover and reorientation tasks rebuilding per-step index and origin tensors with ``repeat`` where a diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/rewards.py index 4abefb912615..36a7eab45f7d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/assemble_trocar/mdp/rewards.py @@ -299,7 +299,7 @@ def get_trocar_tip_position( torch.Tensor: Shape (num_envs, 3) - Position in world coordinates """ # USD is a runtime dependency that must not load at config-import time - from pxr import Gf, Usd, UsdGeom # noqa: PLC0415 + from pxr import Gf, Usd, UsdGeom # Cache the tip offset to avoid recalculating every step. # The local offset from root to tip is a static geometric property of the USD diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env.py index 8a3e55825fc8..2c0547494a24 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/assembly_env.py @@ -692,7 +692,7 @@ def set_pos_inverse_kinematics(self, env_ids): delta_hand_pose = torch.cat((pos_error, axis_angle_error), dim=-1) # Solve DLS problem. - delta_dof_pos = fc._get_delta_dof_pos( + delta_dof_pos = fc.get_delta_dof_pos( delta_pose=delta_hand_pose, ik_method="dls", jacobian=self.fingertip_midpoint_jacobian[env_ids], diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env.py index 519a963bfe43..00cefedde8d1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/disassembly_env.py @@ -521,7 +521,7 @@ def set_pos_inverse_kinematics(self, env_ids): delta_hand_pose = torch.cat((pos_error, axis_angle_error), dim=-1) # Solve DLS problem. - delta_dof_pos = fc._get_delta_dof_pos( + delta_dof_pos = fc.get_delta_dof_pos( delta_pose=delta_hand_pose, ik_method="dls", jacobian=self.fingertip_midpoint_jacobian[env_ids], diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/factory_control.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/factory_control.py index 2d7d98e0fb0e..a57f4624ee3e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/factory_control.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/automate/factory_control.py @@ -133,7 +133,7 @@ def get_pose_error( raise ValueError(f"Unsupported rotation error type: {rot_error_type}. Valid: 'quat', 'axis_angle'.") -def _get_delta_dof_pos(delta_pose, ik_method, jacobian, device): +def get_delta_dof_pos(delta_pose, ik_method, jacobian, device): """Get delta Franka DOF position from delta pose using specified IK method.""" # References: # 1) https://www.cs.cmu.edu/~15464-s13/lectures/lecture6/iksurvey.pdf diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/mdp/events.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/mdp/events.py index 8dde6b5c5105..64a49dc327bd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/mdp/events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/deploy/mdp/events.py @@ -332,7 +332,7 @@ def __call__( jacobians = self.robot_asset.data.body_link_jacobian_w.torch.clone() jacobian = jacobians[env_ids, self.jacobi_body_idx, :, self.robot_asset.num_base_dofs :] - delta_dof_pos = fc._get_delta_dof_pos( + delta_dof_pos = fc.get_delta_dof_pos( delta_pose=delta_hand_pose, ik_method="dls", jacobian=jacobian, diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/__init__.pyi index 530a83615766..7195cd99d9d8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/dr_legs/mdp/__init__.pyi @@ -37,4 +37,6 @@ from .rewards import ( root_orientation_exp, walk_success_rate, ) +# shared terms that used to live in this package, re-exported for backwards compatibility +from isaaclab.envs.mdp import survival_success_rate from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/mdp/observations.py index b89b2c9876cf..22a4663c26ed 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/mdp/observations.py @@ -52,7 +52,7 @@ def tcp_to_grasp_position_c_obs(env: FrankaPourEnv) -> torch.Tensor: return torch.nan_to_num(desired_position_c - tcp_position_c) -def _nearest_grasp_to_tcp_quat(env: FrankaPourEnv) -> torch.Tensor: +def nearest_grasp_to_tcp_quat(env: FrankaPourEnv) -> torch.Tensor: """Return TCP error from the nearest of four equivalent cup-side grasp frames. The source cup is rotationally symmetric for grasping from its four horizontal sides. The @@ -96,7 +96,7 @@ def _nearest_grasp_to_tcp_quat(env: FrankaPourEnv) -> torch.Tensor: def grasp_to_tcp_quat_obs(env: FrankaPourEnv) -> torch.Tensor: """TCP orientation relative to the nearest equivalent source-cup grasp frame.""" - return _nearest_grasp_to_tcp_quat(env) + return nearest_grasp_to_tcp_quat(env) def target_position_c_obs(env: FrankaPourEnv) -> torch.Tensor: diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/mdp/reset_dataset.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/mdp/reset_dataset.py index 420b08d67ba5..2fb330eabacb 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/mdp/reset_dataset.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/mdp/reset_dataset.py @@ -15,7 +15,7 @@ from isaaclab.managers import CurriculumTermCfg from isaaclab.managers.manager_base import ManagerTermBase -from ..reset_sampler import ResetDatasetSamplerCfg, _ResetDatasetSampler +from ..reset_sampler import ResetDatasetSampler, ResetDatasetSamplerCfg if TYPE_CHECKING: from ..pour_env import FrankaPourEnv @@ -42,7 +42,7 @@ def __init__(self, cfg: CurriculumTermCfg, env: FrankaPourEnv): sampler_cfg = env.cfg.reset_dataset_sampler.copy() if not isinstance(sampler_cfg, ResetDatasetSamplerCfg): raise TypeError("reset_dataset_sampler must be ResetDatasetSamplerCfg.") - self._sampler = _ResetDatasetSampler(self._row_count, self._device, sampler_cfg) + self._sampler = ResetDatasetSampler(self._row_count, self._device, sampler_cfg) self._frozen_rows = torch.arange(self._row_count, device=self._device, dtype=torch.long) top_grasp_count = env.cfg.reset_dataset_top_grasp_count diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/mdp/rewards.py index 25c3c2422d86..03ec32211a70 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/mdp/rewards.py @@ -15,7 +15,7 @@ from isaaclab.managers import ManagerTermBase, TerminationTermCfg from isaaclab.utils import math as math_utils -from .observations import _nearest_grasp_to_tcp_quat +from .observations import nearest_grasp_to_tcp_quat if TYPE_CHECKING: from ..pour_env import FrankaPourEnv @@ -57,7 +57,7 @@ def tcp_cup_grasp_pose_tanh( distance = torch.linalg.vector_norm(env.tcp_pose_e()[:, :3] - env.cup_grasp_point_e(), dim=-1) position_quality = 1.0 - torch.tanh(distance / float(position_std)) - error_quat = _nearest_grasp_to_tcp_quat(env) + error_quat = nearest_grasp_to_tcp_quat(env) orientation_error = torch.linalg.vector_norm(math_utils.axis_angle_from_quat(error_quat), dim=-1) orientation_quality = 1.0 - torch.tanh(orientation_error / float(orientation_std)) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/pour_env.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/pour_env.py index e59ff2198d90..d8849a440205 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/pour_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/pour_env.py @@ -41,7 +41,7 @@ TARGET_CUP_GEOMETRY, points_inside_box, ) -from .pour_env_cfg import _configure_mpm_capacities +from .pour_env_cfg import configure_mpm_capacities from .reset_dataset_io import RESET_DATASET_STATE_NAMES, reset_dataset_validate_runtime logger = logging.getLogger(__name__) @@ -109,7 +109,7 @@ class FrankaPourEnv(ManagerBasedRLEnv): cfg: FrankaPourResetDatasetEnvCfg def __init__(self, cfg: FrankaPourResetDatasetEnvCfg, render_mode: str | None = None, **kwargs): - _configure_mpm_capacities(cfg) + configure_mpm_capacities(cfg) self._prepare_newton_extras(cfg) with newton_builder_world_hook(self._add_pour_world_to_builder): super().__init__(cfg, render_mode, **kwargs) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/pour_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/pour_env_cfg.py index 71ac85437b8d..b4198da8bc62 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/pour_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/pour_env_cfg.py @@ -293,7 +293,7 @@ def _resolve_mpm_cell_cap(cfg: FrankaPourResetDatasetEnvCfg) -> int: return capacity -def _configure_mpm_capacities(cfg: FrankaPourResetDatasetEnvCfg) -> None: +def configure_mpm_capacities(cfg: FrankaPourResetDatasetEnvCfg) -> None: """Resolve world-count-dependent MPM capacities after command-line overrides.""" _configure_media_fill(cfg) solver_cfg = _mpm_solver_cfg(cfg) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/reset_sampler.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/reset_sampler.py index 5b626a116af5..db97e66ba9f2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/reset_sampler.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/franka_pour/reset_sampler.py @@ -138,7 +138,7 @@ def _ring_append_bool_count_rate( rate[unique_ids] = new_true_counts.to(rate.dtype) / new_size.clamp(min=1).to(rate.dtype) -class _ResetDatasetSampler: +class ResetDatasetSampler: """Sample reset rows using a rolling-success kernel and exact cyclic replay.""" def __init__( diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/keyboards/keyboard_labels.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/keyboards/keyboard_labels.py index 6d3428cdebd1..fbb31464b3bc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/keyboards/keyboard_labels.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/keyboards/keyboard_labels.py @@ -9,7 +9,7 @@ from .keyboard_schema import KeyboardStyle, ResolvedKey -_FONT_5X7: dict[str, tuple[str, ...]] = { +FONT_5X7: dict[str, tuple[str, ...]] = { "A": ("01110", "10001", "10001", "11111", "10001", "10001", "10001"), "B": ("11110", "10001", "10001", "11110", "10001", "10001", "11110"), "C": ("01111", "10000", "10000", "10000", "10000", "10000", "01111"), @@ -89,7 +89,7 @@ def legend_text(label: str, style: KeyboardStyle) -> str: if style.label_mode == "blank": return "" - text = "".join(ch for ch in label.upper() if ch in _FONT_5X7 or ch.isalnum()).strip() + text = "".join(ch for ch in label.upper() if ch in FONT_5X7 or ch.isalnum()).strip() if not text: return "" compact = text.replace(" ", "") @@ -133,7 +133,7 @@ def label_mesh_data( faces: list[tuple[int, int, int, int]] = [] cursor = 0.0 for ch in text: - glyph = _FONT_5X7.get(ch, _FONT_5X7["?"]) + glyph = FONT_5X7.get(ch, FONT_5X7["?"]) char_cols = 3 if ch == " " else glyph_width if ch != " ": for row_index, row in enumerate(glyph): diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/mdp/commands/typing_commands.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/mdp/commands/typing_commands.py index 061b0f773db4..3e49fa846091 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/mdp/commands/typing_commands.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/mdp/commands/typing_commands.py @@ -28,9 +28,8 @@ skew_symmetric_matrix, ) -from isaaclab_tasks.core.lift.mdp.events import SuccessMonitor -from isaaclab_tasks.core.lift.mdp.events_cfg import SuccessMonitorCfg from isaaclab_tasks.core.lift.mdp.utils import get_reset_state, set_reset_state +from isaaclab_tasks.utils.success_monitor import SuccessMonitor, SuccessMonitorCfg from . import typing_vis diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/mdp/commands/typing_vis.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/mdp/commands/typing_vis.py index e3734bc7e394..329d8c983b2b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/mdp/commands/typing_vis.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/keyboard/mdp/commands/typing_vis.py @@ -6,7 +6,7 @@ """LED dot-matrix visualization helpers for the SO101 keyboard typing command. The Newton marker backend only renders primitive prototypes (sphere/box/cylinder/...), so letters are -drawn as a grid of small cuboid "pixels" using the shared 5x7 bitmap font (``_FONT_5X7``). One +drawn as a grid of small cuboid "pixels" using the shared 5x7 bitmap font (``FONT_5X7``). One cuboid prototype per color encodes the typing state (pending / correct / next / wrong) via the marker prototype index, and a cylinder prototype marks the next key to press. """ @@ -18,7 +18,7 @@ import isaaclab.sim as sim_utils from isaaclab.markers.visualization_markers_cfg import VisualizationMarkersCfg -from ...keyboards.keyboard_labels import _FONT_5X7 +from ...keyboards.keyboard_labels import FONT_5X7 # Marker prototype indices. MUST match the insertion order in :func:`make_typing_visualizer_cfg`. PIX_PENDING = 0 # gray: target letter not yet typed @@ -63,11 +63,11 @@ def _pix(rgb: tuple[float, float, float]) -> sim_utils.CuboidCfg: def build_glyph_table(device: torch.device | str) -> tuple[torch.Tensor, dict[str, int]]: """Return ``(glyph_lit, char_to_index)`` where ``glyph_lit`` is ``(num_chars, 7, 5)`` bool.""" - chars = list(_FONT_5X7.keys()) + chars = list(FONT_5X7.keys()) char_to_index = {ch: i for i, ch in enumerate(chars)} glyph_lit = torch.zeros(len(chars), GLYPH_ROWS, GLYPH_COLS, dtype=torch.bool, device=device) for i, ch in enumerate(chars): - for r, row in enumerate(_FONT_5X7[ch]): + for r, row in enumerate(FONT_5X7[ch]): for c, value in enumerate(row): if value == "1": glyph_lit[i, r, c] = True diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/multitask_manipulation/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/multitask_manipulation/mdp/observations.py index 0f7316e2533f..9743069de676 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/multitask_manipulation/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/multitask_manipulation/mdp/observations.py @@ -14,7 +14,7 @@ from isaaclab.utils import math as math_utils from ..selection_utils import SceneEntitySelectionCfg -from .utils import _offset_body_pose +from .utils import offset_body_pose if TYPE_CHECKING: from isaaclab.assets import Articulation, RigidObject @@ -101,8 +101,8 @@ def cabinet_ee_to_handle( cabinet_cfg: SceneEntitySelectionCfg, ) -> torch.Tensor: """Return the vector [m] from the cabinet Franka TCP to the drawer handle.""" - env_ids, ee_pos_w, _ = _offset_body_pose(env, robot_cfg, (0.0, 0.0, 0.1034)) - _, handle_pos_w, _ = _offset_body_pose( + env_ids, ee_pos_w, _ = offset_body_pose(env, robot_cfg, (0.0, 0.0, 0.1034)) + _, handle_pos_w, _ = offset_body_pose( env, cabinet_cfg, (0.305, 0.0, 0.01), diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/multitask_manipulation/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/multitask_manipulation/mdp/rewards.py index 7b57ea79eb85..15ce72212984 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/multitask_manipulation/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/multitask_manipulation/mdp/rewards.py @@ -15,7 +15,7 @@ from isaaclab.utils import math as math_utils from ..selection_utils import SceneEntitySelectionCfg -from .utils import _offset_body_pose +from .utils import offset_body_pose if TYPE_CHECKING: from isaaclab.assets import Articulation, RigidObject @@ -60,7 +60,7 @@ def lift_ee_object_distance( std: float, ) -> torch.Tensor: """Reward the lift OpenArm for bringing its TCP close to the object.""" - env_ids, tcp_pos_w, _ = _offset_body_pose(env, robot_cfg, (0.0, 0.0, 0.0)) + env_ids, tcp_pos_w, _ = offset_body_pose(env, robot_cfg, (0.0, 0.0, 0.0)) object_asset: RigidObject = env.scene[object_cfg.name] distance = torch.linalg.norm( object_asset.data.root_pos_w.torch[object_cfg.instance_ids[env_ids]] - tcp_pos_w, dim=-1 @@ -158,10 +158,10 @@ def _cabinet_frames( cabinet_cfg: SceneEntitySelectionCfg, ) -> tuple[torch.Tensor, ...]: """Return aligned cabinet-task TCP, handle, and fingertip poses.""" - env_ids, ee_pos, ee_quat = _offset_body_pose(env, robot_cfg, (0.0, 0.0, 0.1034)) - _, handle_pos, handle_quat = _offset_body_pose(env, cabinet_cfg, (0.305, 0.0, 0.01), (0.5, -0.5, -0.5, 0.5)) - _, left_pos, _ = _offset_body_pose(env, robot_cfg, (0.0, 0.0, 0.046), body_index=1) - _, right_pos, _ = _offset_body_pose(env, robot_cfg, (0.0, 0.0, 0.046), body_index=2) + env_ids, ee_pos, ee_quat = offset_body_pose(env, robot_cfg, (0.0, 0.0, 0.1034)) + _, handle_pos, handle_quat = offset_body_pose(env, cabinet_cfg, (0.305, 0.0, 0.01), (0.5, -0.5, -0.5, 0.5)) + _, left_pos, _ = offset_body_pose(env, robot_cfg, (0.0, 0.0, 0.046), body_index=1) + _, right_pos, _ = offset_body_pose(env, robot_cfg, (0.0, 0.0, 0.046), body_index=2) cabinet_rows = cabinet_cfg.instance_ids[env_ids] return env_ids, ee_pos, ee_quat, handle_pos[cabinet_rows], handle_quat[cabinet_rows], left_pos, right_pos diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/multitask_manipulation/mdp/utils.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/multitask_manipulation/mdp/utils.py index d7725d4423bc..c10a3c13d16a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/multitask_manipulation/mdp/utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/multitask_manipulation/mdp/utils.py @@ -20,7 +20,7 @@ from isaaclab.envs import ManagerBasedRLEnv -def _offset_body_pose( +def offset_body_pose( env: ManagerBasedRLEnv, asset_cfg: SceneEntitySelectionCfg, offset_pos: tuple[float, float, float], diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/nist/mdp/events.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/nist/mdp/events.py index c9c1173d5f54..ca8318a15e7c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/nist/mdp/events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/nist/mdp/events.py @@ -17,15 +17,15 @@ from isaaclab.managers import EventTermCfg, ManagerTermBase, SceneEntityCfg from isaaclab.utils import math as math_utils -from isaaclab_tasks.contrib.nist.assembly_keypoints import NIST_BOARD_CFG -from isaaclab_tasks.contrib.nist.assembly_profile_cfg import AssemblyProfileCfg +from ..assembly_keypoints import NIST_BOARD_CFG +from ..assembly_profile_cfg import AssemblyProfileCfg if TYPE_CHECKING: from isaaclab.assets import Articulation, RigidObject from isaaclab.envs import ManagerBasedRLEnv from isaaclab.envs.mdp.actions.task_space_actions import DifferentialInverseKinematicsAction - from isaaclab_tasks.contrib.nist.assembly_keypoints import Offset + from ..assembly_keypoints import Offset def reset_fixed_asset_uniform( diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/exhaustpipe_gr1t2_pink_ik_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/exhaustpipe_gr1t2_pink_ik_env_cfg.py index e2f8ba4c3685..1b90acca2334 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/exhaustpipe_gr1t2_pink_ik_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/exhaustpipe_gr1t2_pink_ik_env_cfg.py @@ -13,7 +13,7 @@ ExhaustPipeGR1T2BaseEnvCfg, ) from isaaclab_tasks.contrib.pick_place.pickplace_gr1t2_env_cfg import ( - _build_gr1t2_pickplace_pipeline, + build_gr1t2_pickplace_pipeline, ) @@ -129,7 +129,7 @@ def __post_init__(self): # IsaacTeleop-based teleoperation pipeline. self.isaac_teleop = IsaacTeleopCfg( - pipeline_builder=lambda: _build_gr1t2_pickplace_pipeline()[0], + pipeline_builder=lambda: build_gr1t2_pickplace_pipeline()[0], sim_device=self.sim.device, xr_cfg=self.xr, xr_camera_feeds=[ diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/nutpour_gr1t2_pink_ik_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/nutpour_gr1t2_pink_ik_env_cfg.py index e5671650ee40..87b49057b99a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/nutpour_gr1t2_pink_ik_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/nutpour_gr1t2_pink_ik_env_cfg.py @@ -11,7 +11,7 @@ from isaaclab_tasks.contrib.pick_place.nutpour_gr1t2_base_env_cfg import NutPourGR1T2BaseEnvCfg from isaaclab_tasks.contrib.pick_place.pickplace_gr1t2_env_cfg import ( - _build_gr1t2_pickplace_pipeline, + build_gr1t2_pickplace_pipeline, ) @@ -127,7 +127,7 @@ def __post_init__(self): # IsaacTeleop-based teleoperation pipeline. self.isaac_teleop = IsaacTeleopCfg( - pipeline_builder=lambda: _build_gr1t2_pickplace_pipeline()[0], + pipeline_builder=lambda: build_gr1t2_pickplace_pipeline()[0], sim_device=self.sim.device, xr_cfg=self.xr, xr_camera_feeds=[ diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/pickplace_gr1t2_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/pickplace_gr1t2_env_cfg.py index 6be0e64b05a5..796b230a4209 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/pickplace_gr1t2_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/pickplace_gr1t2_env_cfg.py @@ -33,7 +33,7 @@ from isaaclab_tasks.contrib.robot_pov_camera_cfg import robot_pov_camera_cfg # isort: skip -def _build_gr1t2_pickplace_pipeline(): +def build_gr1t2_pickplace_pipeline(): """Build an IsaacTeleop retargeting pipeline for GR1T2 pick-place teleoperation. Creates two Se3AbsRetargeters for left and right wrist pose tracking and @@ -665,7 +665,7 @@ def __post_init__(self): anchor_rot=(0.0, 0.0, 0.0, 1.0), ) self.isaac_teleop = IsaacTeleopCfg( - pipeline_builder=lambda: _build_gr1t2_pickplace_pipeline()[0], + pipeline_builder=lambda: build_gr1t2_pickplace_pipeline()[0], sim_device=self.sim.device, xr_cfg=self.xr, xr_camera_feeds=[ diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/pickplace_gr1t2_waist_enabled_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/pickplace_gr1t2_waist_enabled_env_cfg.py index 9cea032486cd..834a0fd3c6de 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/pickplace_gr1t2_waist_enabled_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/pick_place/pickplace_gr1t2_waist_enabled_env_cfg.py @@ -18,7 +18,7 @@ ObjectTableSceneCfg, ObservationsCfg, TerminationsCfg, - _build_gr1t2_pickplace_pipeline, + build_gr1t2_pickplace_pipeline, ) @@ -67,7 +67,7 @@ def __post_init__(self): anchor_rot=(0.0, 0.0, 0.0, 1.0), ) self.isaac_teleop = IsaacTeleopCfg( - pipeline_builder=lambda: _build_gr1t2_pickplace_pipeline()[0], + pipeline_builder=lambda: build_gr1t2_pickplace_pipeline()[0], sim_device=self.sim.device, xr_cfg=self.xr, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/stack_events.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/stack_events.py index 8bad32c07bd7..d6ff4a278abb 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/stack_events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/stack/mdp/stack_events.py @@ -314,11 +314,11 @@ def randomize_visual_texture_material( return # enable replicator extension if not already enabled - from isaaclab.sim.utils import enable_extension # noqa: PLC0415 + from isaaclab.sim.utils import enable_extension enable_extension("omni.replicator.core") # we import the module here since we may not always need the replicator - import omni.replicator.core as rep # noqa: PLC0415 + import omni.replicator.core as rep # check to make sure replicate_physics is set to False, else raise error # note: We add an explicit check here since texture randomization can happen outside of 'prestartup' mode @@ -348,7 +348,7 @@ def randomize_visual_texture_material( if not hasattr(asset, "cfg"): # Static assets carry no runtime view; 'asset' is the spawned cfg. Resolve the prim from # the stage by its spawned path. Local import: keep USD out of module load for pure cfg loading. - from isaaclab.sim.utils import find_matching_prims # noqa: PLC0415 + from isaaclab.sim.utils import find_matching_prims asset_prim_path = find_matching_prims(asset.prim_path)[0].GetPath().pathString prims_group = rep.get.prims(path_pattern=f"{asset_prim_path}/visuals") diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env.py index e91913ed58b4..ea4d6d33bcb9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env.py @@ -16,7 +16,7 @@ from isaaclab.utils.math import combine_frame_transforms, matrix_from_quat if TYPE_CHECKING: - from isaaclab_tasks.core.cabinet.cabinet_direct_env_cfg import CabinetDirectEnvCfg + from .cabinet_direct_env_cfg import CabinetDirectEnvCfg class CabinetDirectEnv(DirectRLEnv): @@ -53,7 +53,7 @@ def __init__(self, cfg: CabinetDirectEnvCfg, render_mode: str | None = None, **k self.arm_joint_targets = torch.zeros((self.num_envs, len(self.arm_joint_ids)), device=self.device) self.finger_joint_targets = torch.zeros((self.num_envs, len(self.finger_joint_ids)), device=self.device) - # frame offsets, broadcast to all environments + # frame offsets, repeated for every environment self.ee_pos_offset = self._repeat_per_env(self.cfg.ee_pos_offset) self.finger_pos_offset = self._repeat_per_env(self.cfg.finger_pos_offset) self.drawer_handle_pos_offset = self._repeat_per_env(self.cfg.drawer_handle_pos_offset) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env_cfg.py index ef8e836cfccf..e0b22ac64065 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_direct_env_cfg.py @@ -14,7 +14,7 @@ from isaaclab.scene import InteractiveSceneCfg from isaaclab.utils import configclass -from isaaclab_tasks.core.cabinet.cabinet_env_cfg import ( +from .cabinet_env_cfg import ( CABINET_CFG, LIGHT_CFG, PLANE_CFG, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_env_cfg.py index 3488fc8e3d08..c9806102dda6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/cabinet_env_cfg.py @@ -31,9 +31,10 @@ from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR from isaaclab.visualizers import VisualizerCfg -import isaaclab_tasks.core.cabinet.mdp as mdp from isaaclab_tasks.utils import PresetCfg +from . import mdp + ## # Scene assets ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/cabinet_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/cabinet_direct_env_cfg.py index 1e3516d16724..48f1f174eef4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/cabinet_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/cabinet_direct_env_cfg.py @@ -9,10 +9,10 @@ from isaaclab.utils import configclass -from isaaclab_tasks.core.cabinet.cabinet_direct_env_cfg import CabinetDirectEnvCfg, CabinetDirectSceneCfg - from isaaclab_assets.robots.franka import FRANKA_PANDA_CFG +from ...cabinet_direct_env_cfg import CabinetDirectEnvCfg, CabinetDirectSceneCfg + @configclass class FrankaCabinetDirectSceneCfg(CabinetDirectSceneCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/joint_pos_env_cfg.py index 5b2930b9a7b7..95c3950ed2ef 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cabinet/config/franka/joint_pos_env_cfg.py @@ -9,11 +9,11 @@ from isaaclab.sensors.frame_transformer import OffsetCfg from isaaclab.utils import configclass -import isaaclab_tasks.core.cabinet.mdp as mdp -from isaaclab_tasks.core.cabinet.cabinet_env_cfg import FRAME_MARKER_SMALL_CFG, CabinetEnvCfg, CabinetSceneCfg - from isaaclab_assets.robots.franka import FRANKA_PANDA_CFG +from ... import mdp +from ...cabinet_env_cfg import FRAME_MARKER_SMALL_CFG, CabinetEnvCfg, CabinetSceneCfg + @configclass class FrankaCabinetSceneCfg(CabinetSceneCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/agents/rsl_rl_ppo_cfg.py index d0d8d22efe7d..c5d74007253e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/agents/rsl_rl_ppo_cfg.py @@ -13,9 +13,10 @@ RslRlSymmetryCfg, ) -import isaaclab_tasks.core.cartpole.mdp.symmetry as symmetry from isaaclab_tasks.utils import PresetCfg +from ..mdp import symmetry + @configclass class CartpolePPORunnerCfg(RslRlOnPolicyRunnerCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_common.py index 6c02e97ba33a..c2583898c610 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_common.py @@ -23,7 +23,7 @@ -0.3535534143447876, 0.8535533547401428, ) -"""Distant light orientation as an ``(x, y, z, w)`` quaternion for euler angles (0, -45, -45) degrees.""" +"""Distant light orientation as an ``(x, y, z, w)`` quaternion for roll, pitch, yaw = (0, -45, -45) degrees.""" @configclass diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env.py index 06a3bbfe7b71..a0aca79ccf3d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env.py @@ -16,10 +16,10 @@ from isaaclab.utils.buffers import CircularBuffer from isaaclab.utils.images import is_rgb_like, normalize_camera_image -from isaaclab_tasks.core.cartpole.cartpole_direct_env import CartpoleEnv +from .cartpole_direct_env import CartpoleEnv if TYPE_CHECKING: - from isaaclab_tasks.core.cartpole.cartpole_direct_camera_env_cfg import CartpoleCameraEnvCfg + from .cartpole_direct_camera_env_cfg import CartpoleCameraEnvCfg class CartpoleCameraEnv(CartpoleEnv): @@ -94,7 +94,8 @@ def _get_observations(self) -> dict: if self.cfg.write_image_to_file: save_images_to_file(self._tiled_camera.data.output[data_type] / 255.0, f"cartpole_{data_type}.png") - return {"policy": obs, "critic": super()._get_observations()["policy"]} + critic_obs = super()._get_observations()["policy"] + return {"policy": obs, "critic": critic_obs} def _reset_idx(self, env_ids: Sequence[int] | None): super()._reset_idx(env_ids) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py index 5b406b396a7b..1b0086229176 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_camera_env_cfg.py @@ -12,10 +12,11 @@ from isaaclab.utils import configclass from isaaclab.visualizers import VisualizerCfg -from isaaclab_tasks.core.cartpole.cartpole_common import CartpoleTiledCameraCfg -from isaaclab_tasks.core.cartpole.cartpole_direct_env_cfg import CartpoleEnvCfg, CartpoleSceneCfg from isaaclab_tasks.utils import PresetCfg +from .cartpole_common import CartpoleTiledCameraCfg +from .cartpole_direct_env_cfg import CartpoleEnvCfg, CartpoleSceneCfg + @configclass class CartpoleCameraSceneCfg(CartpoleSceneCfg): @@ -59,6 +60,7 @@ class BaseCartpoleCameraEnvCfg(CartpoleEnvCfg): def __post_init__(self): super().__post_init__() + # the base sets the proprioceptive camera view; widen it to see the camera environments self.sim.default_visualizer_cfg = VisualizerCfg(eye=(20.0, 20.0, 20.0)) default = BaseCartpoleCameraEnvCfg() diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env.py index 085ad2c13033..cb222eeb3068 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env.py @@ -16,7 +16,7 @@ from isaaclab.utils.math import sample_uniform, wrap_to_pi if TYPE_CHECKING: - from isaaclab_tasks.core.cartpole.cartpole_direct_env_cfg import CartpoleEnvCfg + from .cartpole_direct_env_cfg import CartpoleEnvCfg class CartpoleEnv(DirectRLEnv): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env_cfg.py index 10b447f84d9f..773f60eee7a0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_direct_env_cfg.py @@ -17,10 +17,10 @@ from isaaclab.utils import configclass from isaaclab.visualizers import VisualizerCfg -from isaaclab_tasks.core.cartpole.cartpole_common import LIGHT_ORIENTATION, CartpolePhysicsCfg - from isaaclab_assets.robots.cartpole import CARTPOLE_CFG +from .cartpole_common import LIGHT_ORIENTATION, CartpolePhysicsCfg + @configclass class CartpoleSceneCfg(InteractiveSceneCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_camera_env_cfg.py index cfe416e10c8a..44638aae20e8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_camera_env_cfg.py @@ -13,11 +13,12 @@ from isaaclab.utils import configclass from isaaclab.visualizers import VisualizerCfg -import isaaclab_tasks.core.cartpole.mdp as mdp -from isaaclab_tasks.core.cartpole.cartpole_common import CartpoleTiledCameraCfg -from isaaclab_tasks.core.cartpole.cartpole_manager_env_cfg import CartpoleEnvCfg, CartpoleSceneCfg, ObservationsCfg from isaaclab_tasks.utils import PresetCfg +from . import mdp +from .cartpole_common import CartpoleTiledCameraCfg +from .cartpole_manager_env_cfg import CartpoleEnvCfg, CartpoleSceneCfg, ObservationsCfg + ## # Scene definition ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_env_cfg.py index 8b096733ed8d..a5c88652f3e6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/cartpole_manager_env_cfg.py @@ -20,11 +20,11 @@ from isaaclab.utils import configclass from isaaclab.visualizers import VisualizerCfg -import isaaclab_tasks.core.cartpole.mdp as mdp -from isaaclab_tasks.core.cartpole.cartpole_common import LIGHT_ORIENTATION, CartpolePhysicsCfg - from isaaclab_assets.robots.cartpole import CARTPOLE_CFG +from . import mdp +from .cartpole_common import LIGHT_ORIENTATION, CartpolePhysicsCfg + ## # Scene definition ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/__init__.pyi index 49ffda014ee7..10fd02c951f0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/cartpole/mdp/__init__.pyi @@ -7,4 +7,6 @@ __all__ = ["CameraImageStack"] from .observations import CameraImageStack +# shared terms that used to live in this package, re-exported for backwards compatibility +from isaaclab.envs.mdp import joint_pos_target_l2, survival_success_rate from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/fourbar_pole_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/fourbar_pole_manager_env_cfg.py index 3eaa5450d760..e9bd93a5945c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/fourbar_pole_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/fourbar_pole/fourbar_pole_manager_env_cfg.py @@ -26,9 +26,10 @@ from isaaclab.utils import configclass from isaaclab.visualizers import VisualizerCfg -import isaaclab_tasks.core.fourbar_pole.mdp as mdp from isaaclab_tasks.utils import PresetCfg +from . import mdp + ## # Pre-defined configs ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env.py index 37fad7397936..977cb9f2dff4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env.py @@ -16,8 +16,6 @@ from isaaclab.envs import DirectMARLEnv from isaaclab.utils.math import quat_conjugate, quat_mul, sample_uniform, saturate, scale_transform, unscale_transform -from isaaclab_tasks.core.handover.handover_common import GOAL_POSITION_OFFSET -from isaaclab_tasks.core.handover.mdp.rewards import evaluate_handover_success, handover_reward from isaaclab_tasks.core.reorient.utils import ( EpisodeErrorRecorder, randomize_rotation, @@ -25,8 +23,11 @@ sample_joint_positions_within_limits, ) +from .handover_common import GOAL_POSITION_OFFSET +from .mdp.rewards import evaluate_handover_success, handover_reward + if TYPE_CHECKING: - from isaaclab_tasks.core.handover.handover_env_cfg import HandoverEnvCfg + from .handover_env_cfg import HandoverEnvCfg class HandoverEnv(DirectMARLEnv): @@ -329,10 +330,9 @@ def _reset_target_pose(self, env_ids: Sequence[int] | torch.Tensor) -> None: ) def _compute_intermediate_values(self) -> None: + env_origins = self.scene.env_origins.unsqueeze(1) # data for right hand - self.right_fingertip_pos = self.right_hand.data.body_pos_w.torch[ - :, self.finger_bodies - ] - self.scene.env_origins.unsqueeze(1) + self.right_fingertip_pos = self.right_hand.data.body_pos_w.torch[:, self.finger_bodies] - env_origins self.right_fingertip_rot = self.right_hand.data.body_quat_w.torch[:, self.finger_bodies] self.right_fingertip_velocities = self.right_hand.data.body_vel_w.torch[:, self.finger_bodies] @@ -340,9 +340,7 @@ def _compute_intermediate_values(self) -> None: self.right_hand_dof_vel = self.right_hand.data.joint_vel.torch # data for left hand - self.left_fingertip_pos = self.left_hand.data.body_pos_w.torch[ - :, self.finger_bodies - ] - self.scene.env_origins.unsqueeze(1) + self.left_fingertip_pos = self.left_hand.data.body_pos_w.torch[:, self.finger_bodies] - env_origins self.left_fingertip_rot = self.left_hand.data.body_quat_w.torch[:, self.finger_bodies] self.left_fingertip_velocities = self.left_hand.data.body_vel_w.torch[:, self.finger_bodies] diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py index d9fd0b2d3efb..2aaf97b942c6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py @@ -24,7 +24,6 @@ from isaaclab.utils import math as math_utils from isaaclab.visualizers import VisualizerCfg -from isaaclab_tasks.core.handover.handover_common import GOAL_MARKER_CFG, OBJECT_RADIUS from isaaclab_tasks.utils import PresetCfg from isaaclab_assets.robots.shadow_hand import ( @@ -36,6 +35,8 @@ TENDON_POSITION_LIMITS, ) +from .handover_common import GOAL_MARKER_CFG, OBJECT_RADIUS + def _hand_cfg( base: ArticulationCfg, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py index f4f3200846a2..67f5e716f0e9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py @@ -19,13 +19,14 @@ from isaaclab.utils import configclass from isaaclab.visualizers import VisualizerCfg -import isaaclab_tasks.core.handover.mdp as mdp import isaaclab_tasks.core.reorient.mdp as reorient_mdp -from isaaclab_tasks.core.handover.handover_env_cfg import BALL_CFG, LeftHandCfg, PhysicsCfg, RightHandCfg from isaaclab_tasks.utils import PresetCfg from isaaclab_assets.robots.shadow_hand import FINGERTIP_NAMES, JOINT_NAMES, TENDON_NAMES, TENDON_POSITION_LIMITS +from . import mdp +from .handover_env_cfg import BALL_CFG, LeftHandCfg, PhysicsCfg, RightHandCfg + ## # Scene definition ## @@ -65,7 +66,7 @@ class CommandsCfg: TENDON_ACTION_CFG = mdp.FixedTendonPositionActionCfg( - asset_name="robot", + asset_name="robot", # placeholder, replaced per hand below tendon_names=TENDON_NAMES, # four of the twenty motors pull a tendon across a finger's middle and distal joints; tendons have # their own index space, so no joint term can reach them. Map the policy's [-1, 1] onto the @@ -74,7 +75,7 @@ class CommandsCfg: offset=0.5 * (TENDON_POSITION_LIMITS[0] + TENDON_POSITION_LIMITS[1]), clip={".*": TENDON_POSITION_LIMITS}, ) -"""Tendon position action term of one hand, before the asset name is set.""" +"""Tendon position action term shared by both hands; ``asset_name`` is replaced per hand.""" @configclass @@ -114,10 +115,10 @@ class PolicyCfg(ObsGroup): func=reorient_mdp.fingertip_vel, params={"asset_cfg": SceneEntityCfg("right_hand", body_names=FINGERTIP_NAMES)}, ) + right_action = ObsTerm(func=mdp.last_action, params={"action_name": "right_hand"}) # a hand's motors span two action terms, so its previous command does too: without the tendon # term the policy sees 16 of the 20 actions it took, and the group falls 4 short of the 133 # hand dimensions the Direct task lays out - right_action = ObsTerm(func=mdp.last_action, params={"action_name": "right_hand"}) right_tendon_action = ObsTerm(func=mdp.last_action, params={"action_name": "right_hand_tendons"}) object_pos = ObsTerm(func=mdp.root_pos_w, params={"asset_cfg": SceneEntityCfg("object")}) object_quat = ObsTerm(func=mdp.root_quat_w, params={"asset_cfg": SceneEntityCfg("object")}) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands_cfg.py index 3354dab6b113..ac243cd0bf7f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands_cfg.py @@ -13,8 +13,7 @@ from isaaclab.markers import VisualizationMarkersCfg from isaaclab.utils import configclass -from isaaclab_tasks.core.handover.handover_common import GOAL_MARKER_CFG, GOAL_POSITION_OFFSET - +from ..handover_common import GOAL_MARKER_CFG, GOAL_POSITION_OFFSET from .commands import HandoverCommand diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/adr_curriculum.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/adr_curriculum.py index f9f016a268ae..706cd7c9b4bd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/adr_curriculum.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/adr_curriculum.py @@ -8,7 +8,7 @@ from isaaclab.managers import CurriculumTermCfg as CurrTerm from isaaclab.utils import configclass -import isaaclab_tasks.core.lift.mdp as mdp +from . import mdp @configclass diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka/franka_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka/franka_env_cfg.py index 06c1afa57bce..47da7ee1671c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka/franka_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka/franka_env_cfg.py @@ -16,11 +16,11 @@ from isaaclab.utils import configclass from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR -import isaaclab_tasks.core.lift.lift_env_cfg as lift -import isaaclab_tasks.core.lift.mdp as mdp - from isaaclab_assets.robots import FRANKA_PANDA_CFG +from ... import lift_env_cfg as lift +from ... import mdp + ## # Scene assets ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cable_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cable_env_cfg.py index c4a859463b41..73344d1d7772 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cable_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cable_env_cfg.py @@ -25,10 +25,11 @@ from isaaclab_contrib.coupling import CouplerEntryCfg, CouplerProxyCfg, CouplerProxyMappingCfg -import isaaclab_tasks.core.lift.mdp as mdp -from isaaclab_tasks.core.lift.config.franka_soft import franka_soft_env_cfg as soft from isaaclab_tasks.utils import PresetCfg +from ... import mdp +from . import franka_soft_env_cfg as soft + _CABLE_SEGMENT_COUNT = 12 _CABLE_MIDDLE_SEGMENT_INDEX = _CABLE_SEGMENT_COUNT // 2 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py index 8e0f552c6b33..2b05c4af617b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_cloth_env_cfg.py @@ -32,10 +32,11 @@ from isaaclab_contrib.coupling import CouplerEntryCfg, CouplerProxyCfg, CouplerProxyMappingCfg -import isaaclab_tasks.core.lift.mdp as mdp -from isaaclab_tasks.core.lift.config.franka_soft import franka_soft_env_cfg as soft from isaaclab_tasks.utils import PresetCfg +from ... import mdp +from . import franka_soft_env_cfg as soft + ## # Physics backend presets ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py index 97dc870b4fbe..4ac518f4b8df 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/franka_soft/franka_soft_env_cfg.py @@ -45,12 +45,13 @@ from isaaclab_contrib.coupling import CouplerEntryCfg, CouplerProxyCfg, CouplerProxyMappingCfg -import isaaclab_tasks.core.lift.mdp as mdp from isaaclab_tasks.utils import PresetCfg, preset from isaaclab_tasks.utils.presets import MultiBackendRendererCfg from isaaclab_assets.robots.franka import FRANKA_PANDA_MENAGERIE_CFG +from ... import mdp + ## # Scene assets ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py index c48ad94563f5..c32cb427ae75 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py @@ -15,11 +15,12 @@ from isaaclab.utils import configclass from isaaclab.utils.noise import UniformNoiseCfg as Unoise -import isaaclab_tasks.core.lift.lift_env_cfg as lift -import isaaclab_tasks.core.lift.mdp as mdp from isaaclab_tasks.utils import PresetCfg from isaaclab_tasks.utils.presets import MultiBackendRendererCfg +from ... import lift_env_cfg as lift +from ... import mdp + FINGERTIP_LIST = ["index_link_3", "middle_link_3", "ring_link_3", "thumb_link_3"] """Fingertip bodies that carry an object contact sensor.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_camera_env_cfg.py index f44ba89039d8..b674fc9a91f6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_camera_env_cfg.py @@ -14,18 +14,19 @@ from isaaclab.sensors import CameraCfg from isaaclab.utils import configclass -from isaaclab_tasks.core.lift.config.kuka_allegro.camera_cfg import ( +from isaaclab_tasks.utils import PresetCfg + +from .camera_cfg import ( BaseTiledCameraCfg, DuoCameraObservationsCfg, SingleCameraObservationsCfg, WristTiledCameraCfg, ) -from isaaclab_tasks.core.lift.config.kuka_allegro.kuka_allegro_env_cfg import ( +from .kuka_allegro_env_cfg import ( KukaAllegroLiftEnvCfg, KukaAllegroReorientEnvCfg, KukaAllegroSceneCfg, ) -from isaaclab_tasks.utils import PresetCfg _SCENE_KWARGS = {"num_envs": 4096, "env_spacing": 3, "replicate_physics": True} diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_env_cfg.py index f8bee6c03478..514e79cf20c7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/kuka_allegro_env_cfg.py @@ -12,12 +12,12 @@ from isaaclab.sensors import CameraCfg, ContactSensorCfg from isaaclab.utils import configclass -import isaaclab_tasks.core.lift.lift_env_cfg as lift -import isaaclab_tasks.core.lift.mdp as mdp -from isaaclab_tasks.core.lift.config.kuka_allegro.camera_cfg import FINGERTIP_LIST, StateObservationCfg - from isaaclab_assets.robots import KUKA_ALLEGRO_CFG +from ... import lift_env_cfg as lift +from ... import mdp +from .camera_cfg import FINGERTIP_LIST, StateObservationCfg + THUMB_SENSOR = "thumb_link_3_object_s" """Contact sensor of the thumb.""" 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 71f9b6f518c7..04f1f6996ab7 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 @@ -30,10 +30,11 @@ from isaaclab.utils.noise import UniformNoiseCfg as Unoise from isaaclab.visualizers import VisualizerCfg -import isaaclab_tasks.core.lift.mdp as mdp -from isaaclab_tasks.core.lift.adr_curriculum import CurriculumCfg from isaaclab_tasks.utils import PresetCfg +from . import mdp +from .adr_curriculum import CurriculumCfg + ## # Scene assets ## @@ -590,8 +591,10 @@ def __post_init__(self): self.decimation = 4 # 30 Hz self.episode_length_s = 12.0 self.is_finite_horizon = False + # commands: track the full pose self.commands.object_pose.position_only = False + # simulation settings self.sim.dt = 1 / 120 self.sim.render_interval = self.decimation diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/__init__.pyi index 757dabf55653..e5826e6db8f4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/__init__.pyi @@ -114,4 +114,6 @@ from .terminations import ( ) from .utils import get_reset_state, set_reset_state +# shared terms that used to live in this package, re-exported for backwards compatibility +from isaaclab.envs.mdp import DifficultyScheduler, initial_final_interpolate_fn from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/pose_commands.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/pose_commands.py index 9e3121393c12..32b9267949bd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/pose_commands.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/commands/pose_commands.py @@ -76,7 +76,8 @@ def __init__(self, cfg: ObjectUniformPoseCommandCfg, env: ManagerBasedEnv): self.metrics["position_error"] = torch.zeros(self.num_envs, device=self.device) if not self.cfg.position_only: self.metrics["orientation_error"] = torch.zeros(self.num_envs, device=self.device) - # -- success markers, always visible + + # success markers, always visible self.success_visualizer = VisualizationMarkers(self.cfg.success_visualizer_cfg) self.success_visualizer.set_visibility(True) if self.success_vis_asset is not None: diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/observations.py index 2fcf26d0b8dc..780a1dad8ac6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/observations.py @@ -144,10 +144,11 @@ def __call__( env: The environment. ref_asset_cfg: Scene entity providing the reference (root) frame. Defaults to ``SceneEntityCfg("robot")``. object_cfg: Scene entity of the object to sample. Defaults to ``SceneEntityCfg("object")``. - num_points: Number of surface points. Read at construction; changing it at runtime has no effect. + num_points: Number of surface points. Must match the value the points were sampled with at construction. flatten: Whether to return the points as ``(num_envs, 3 * num_points)`` instead of ``(num_envs, num_points, 3)``. - visualize: Whether to draw markers for the points. Read at construction and per call. + visualize: Whether to draw markers for the points. The markers are only created when this is + ``True`` in the term parameters. Returns: Object surface points [m] in the reference root frame, flattened if requested. @@ -210,7 +211,10 @@ def __call__(self, env: ManagerBasedRLEnv, sensor_cfg: SceneEntityCfg, normalize images = self.sensor.data.output[self.sensor_type] torch.nan_to_num_(images, nan=1e6) if normalize: - images = torch.tanh(images / 2) - 0.5 if self._is_depth else images.float() / 255.0 - 0.5 + if self._is_depth: + images = torch.tanh(images / 2) - 0.5 + else: + images = images.float() / 255.0 - 0.5 images = images.permute(0, 3, 1, 2).contiguous() return images diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/rewards.py index 7c3e0d14e669..ce1e42e79769 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/rewards.py @@ -105,7 +105,6 @@ class success_reward(ManagerTermBase): Maintains a sticky ``succeeded`` boolean tensor per environment that flips to ``True`` once the success condition is met during an episode and resets to ``False`` on environment reset. - """ def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): @@ -406,7 +405,7 @@ def deformable_com_ee_distance( class _GoalDistanceReward(ManagerTermBase): """Base class for goal-distance rewards that also log the episode success rate. - Subclasses set :attr:`_succeeded` in ``__call__``; the flag is flushed to ``Metrics/success_rate`` + Subclasses set ``_succeeded`` in ``__call__``; the flag is flushed to ``Metrics/success_rate`` in ``extras["log"]`` on reset. """ diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/utils.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/utils.py index d0ba2b24fdcd..b2cd05dff69a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/mdp/utils.py @@ -54,10 +54,10 @@ def sample_object_point_cloud(num_envs: int, num_points: int, prim_path: str, de Surface points [m] in the object root frame, shape ``(num_envs, num_points, 3)``. """ # USD and trimesh are runtime dependencies that must not load at config-import time - import trimesh # noqa: PLC0415 - from trimesh.sample import sample_surface # noqa: PLC0415 + import trimesh + from trimesh.sample import sample_surface - from pxr import UsdGeom # noqa: PLC0415 + from pxr import UsdGeom points = torch.zeros((num_envs, num_points, 3), dtype=torch.float32, device=device) xform_cache = UsdGeom.XformCache() @@ -198,7 +198,7 @@ def sample_object_point_cloud(num_envs: int, num_points: int, prim_path: str, de def _triangulate_faces(prim) -> np.ndarray: """Convert a USD Mesh prim into triangulated face indices (N, 3).""" - from pxr import UsdGeom # noqa: PLC0415 + from pxr import UsdGeom mesh = UsdGeom.Mesh(prim) counts = mesh.GetFaceVertexCountsAttr().Get() @@ -214,9 +214,9 @@ def _triangulate_faces(prim) -> np.ndarray: def create_primitive_mesh(prim) -> trimesh.Trimesh: """Create a trimesh mesh from a USD primitive (Cube, Sphere, Cylinder, etc.).""" - import trimesh # noqa: PLC0415 + import trimesh - from pxr import UsdGeom # noqa: PLC0415 + from pxr import UsdGeom prim_type = prim.GetTypeName() if prim_type == "Cube": @@ -297,11 +297,11 @@ def collect_collision_meshes(root_prim, owner_frame_fn: Callable) -> dict[int, t Returns: One merged mesh per owner key. """ - import trimesh # noqa: PLC0415 + import trimesh - from pxr import UsdPhysics # noqa: PLC0415 + from pxr import UsdPhysics - from isaaclab.utils.mesh import ( # noqa: PLC0415 + from isaaclab.utils.mesh import ( PRIMITIVE_MESH_TYPES, create_trimesh_from_geom_mesh, create_trimesh_from_geom_shape, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env.py index 0c2ff59d38d8..cc68c0f9c616 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env.py @@ -7,8 +7,8 @@ from __future__ import annotations -from isaaclab_tasks.core.locomotion.ant.ant_direct_env_cfg import AntEnvCfg -from isaaclab_tasks.core.locomotion.locomotion_direct_env import LocomotionDirectEnv +from ..locomotion_direct_env import LocomotionDirectEnv +from .ant_direct_env_cfg import AntEnvCfg class AntEnv(LocomotionDirectEnv): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env_cfg.py index cb75488588b1..e631d86f99d8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_direct_env_cfg.py @@ -15,7 +15,9 @@ from isaaclab.sim import SimulationCfg from isaaclab.utils import configclass -from isaaclab_tasks.core.locomotion.ant.ant_common import ( +from isaaclab_assets.robots.ant import ANT_CFG + +from .ant_common import ( FEET_BODY_NAMES, JOINT_GEARS, TERRAIN_CFG, @@ -23,8 +25,6 @@ AntPhysicsCfg, ) -from isaaclab_assets.robots.ant import ANT_CFG - @configclass class AntDirectSceneCfg(InteractiveSceneCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_manager_env_cfg.py index 44ebace6aca4..f4f33c379fb7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/ant/ant_manager_env_cfg.py @@ -18,8 +18,10 @@ from isaaclab.sensors import JointWrenchSensorCfg from isaaclab.utils import configclass -import isaaclab_tasks.core.locomotion.mdp as mdp -from isaaclab_tasks.core.locomotion.ant.ant_common import ( +from isaaclab_assets.robots.ant import ANT_CFG + +from .. import mdp +from .ant_common import ( FEET_BODY_NAMES, JOINT_GEARS, TERRAIN_CFG, @@ -27,8 +29,6 @@ AntPhysicsCfg, ) -from isaaclab_assets.robots.ant import ANT_CFG - ## # Scene definition ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_common.py index e08f7e9bebb0..c02ab016008e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_common.py @@ -9,11 +9,28 @@ from isaaclab_ov.physics import OvPhysxCfg from isaaclab_physx.physics import PhysxCfg +import isaaclab.sim as sim_utils from isaaclab.physics import PhysxAutoCfg +from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass from isaaclab_tasks.utils import PresetCfg +TERRAIN_CFG = TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="plane", + collision_group=-1, + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="average", + restitution_combine_mode="average", + static_friction=1.0, + dynamic_friction=1.0, + restitution=0.0, + ), + debug_vis=False, +) +"""Flat ground plane the Humanoid walks on.""" + JOINT_GEARS: dict[str, float] = { ".*_waist.*": 67.5, ".*_upper_arm.*": 67.5, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env.py index 3214023e6b42..2a8d8684ec73 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env.py @@ -7,8 +7,8 @@ from __future__ import annotations -from isaaclab_tasks.core.locomotion.humanoid.humanoid_direct_env_cfg import HumanoidEnvCfg -from isaaclab_tasks.core.locomotion.locomotion_direct_env import LocomotionDirectEnv +from ..locomotion_direct_env import LocomotionDirectEnv +from .humanoid_direct_env_cfg import HumanoidEnvCfg class HumanoidEnv(LocomotionDirectEnv): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env_cfg.py index 17216a9d8469..bf05f3939552 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_direct_env_cfg.py @@ -15,16 +15,10 @@ from isaaclab.sim import SimulationCfg from isaaclab.utils import configclass -from isaaclab_tasks.core.locomotion.ant.ant_common import TERRAIN_CFG -from isaaclab_tasks.core.locomotion.humanoid.humanoid_common import ( - FEET_BODY_NAMES, - JOINT_GEARS, - WALK_TARGET_POS, - HumanoidPhysicsCfg, -) - from isaaclab_assets.robots.humanoid import HUMANOID_CFG +from .humanoid_common import FEET_BODY_NAMES, JOINT_GEARS, TERRAIN_CFG, WALK_TARGET_POS, HumanoidPhysicsCfg + @configclass class HumanoidDirectSceneCfg(InteractiveSceneCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_manager_env_cfg.py index d9d2acc1a12f..6fb189ed9cc6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/humanoid/humanoid_manager_env_cfg.py @@ -19,8 +19,10 @@ from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass -import isaaclab_tasks.core.locomotion.mdp as mdp -from isaaclab_tasks.core.locomotion.humanoid.humanoid_common import ( +from isaaclab_assets.robots.humanoid import HUMANOID_CFG + +from .. import mdp +from .humanoid_common import ( FEET_BODY_NAMES, JOINT_EFFORT_LIMITS, JOINT_GEARS, @@ -28,8 +30,6 @@ HumanoidPhysicsCfg, ) -from isaaclab_assets.robots.humanoid import HUMANOID_CFG - ## # Scene definition ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/locomotion_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/locomotion_direct_env.py index b614f235cd7e..e9d18b0a0aa6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/locomotion_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/locomotion_direct_env.py @@ -24,8 +24,8 @@ from isaaclab.utils.string import resolve_matching_names_values if TYPE_CHECKING: - from isaaclab_tasks.core.locomotion.ant.ant_direct_env_cfg import AntEnvCfg - from isaaclab_tasks.core.locomotion.humanoid.humanoid_direct_env_cfg import HumanoidEnvCfg + from .ant.ant_direct_env_cfg import AntEnvCfg + from .humanoid.humanoid_direct_env_cfg import HumanoidEnvCfg class LocomotionDirectEnv(DirectRLEnv): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/__init__.pyi index c54e760921a8..6c07dd328a1f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/__init__.pyi @@ -24,4 +24,6 @@ from .rewards import ( upright_posture_bonus, ) +# shared terms that used to live in this package, re-exported for backwards compatibility +from isaaclab.envs.mdp import survival_success_rate, terminated_penalty from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/rewards.py index 9818129b0d95..f5ad55121d59 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/locomotion/mdp/rewards.py @@ -15,7 +15,7 @@ import isaaclab.utils.string as string_utils from isaaclab.managers import ManagerTermBase, RewardTermCfg, SceneEntityCfg -import isaaclab_tasks.core.locomotion.mdp.observations as obs +from . import observations as obs if TYPE_CHECKING: from isaaclab.assets import Articulation @@ -111,9 +111,12 @@ def __call__( class power_consumption(ManagerTermBase): - """Penalty for the power consumed by the actions to the environment. + """Penalty for the power consumed by the joint actions. - This is computed as commanded torque times the joint velocity. + Computed as the action scaled by its gear ratio, normalized by the largest gear ratio, times the joint + velocity, summed over joints. The effort action terms scale the actions by the same gear ratios, so up + to that normalization and the effort clip this is the commanded effort times the joint velocity. It + matches the electricity cost of the direct locomotion environments. """ def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): @@ -127,7 +130,7 @@ def __call__( ) -> torch.Tensor: # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] - # return power = torque * velocity (here actions: joint torques) + # power = effort * velocity, with the effort taken as the gear-normalized action return torch.sum( torch.abs(env.action_manager.action * asset.data.joint_vel.torch * self.gear_ratio_scaled), dim=-1 ) @@ -144,7 +147,8 @@ def _resolve_scaled_gear_ratio(gear_ratio: dict[str, float], asset: Articulation device: Device of the returned tensor. Returns: - Gear ratios divided by the maximum gear ratio, shape ``(num_joints,)``. + Gear ratios divided by the maximum gear ratio, shape ``(num_joints,)``. Broadcasts against the + ``(num_envs, num_joints)`` joint tensors. """ gears = torch.ones(asset.num_joints, device=device) joint_ids, _, values = string_utils.resolve_matching_names_values(gear_ratio, asset.joint_names) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/pendulum_marl_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/pendulum_marl_env.py index 91d9b7c17e16..b0f2df647d18 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/pendulum_marl_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/pendulum/pendulum_marl_env.py @@ -13,7 +13,7 @@ from isaaclab.envs import DirectMARLEnv from isaaclab.utils.math import sample_uniform -from isaaclab_tasks.core.pendulum.pendulum_marl_env_cfg import PendulumMARLEnvCfg +from .pendulum_marl_env_cfg import PendulumMARLEnvCfg class PendulumMARLEnv(DirectMARLEnv): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_env_cfg.py index 484c5f1e8f57..9df2f4f6e87a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_env_cfg.py @@ -23,11 +23,12 @@ from isaaclab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg from isaaclab.utils import configclass -from isaaclab_tasks.core.reach.reach_env_cfg import ReachEnvCfg from isaaclab_tasks.utils import PresetCfg, preset from isaaclab_assets import FRANKA_PANDA_CFG, FRANKA_PANDA_MENAGERIE_CFG +from ...reach_env_cfg import ReachEnvCfg + ## # Environment configuration ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_osc_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_osc_env_cfg.py index 19c2f194e70d..09c6d8fab2cd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_osc_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/franka/franka_reach_osc_env_cfg.py @@ -15,9 +15,10 @@ from isaaclab.envs.mdp.actions.actions_cfg import OperationalSpaceControllerActionCfg from isaaclab.utils import configclass -from isaaclab_tasks.core.reach.config.franka import franka_reach_env_cfg from isaaclab_tasks.utils import preset +from . import franka_reach_env_cfg + @configclass class FrankaReachEnvCfg(franka_reach_env_cfg.FrankaReachEnvCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/ur_10/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/ur_10/joint_pos_env_cfg.py index 4a9af7b83e39..c0ec63915e20 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/ur_10/joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reach/config/ur_10/joint_pos_env_cfg.py @@ -10,10 +10,10 @@ import isaaclab.envs.mdp as mdp from isaaclab.utils import configclass -from isaaclab_tasks.core.reach.reach_env_cfg import ReachEnvCfg - from isaaclab_assets import UR10_CFG +from ...reach_env_cfg import ReachEnvCfg + ## # Environment configuration ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_direct_env_cfg.py index 02b7543eb89a..b9beca7af2a8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_direct_env_cfg.py @@ -15,15 +15,15 @@ from isaaclab.sim.spawners.materials import RigidBodyMaterialBaseCfg from isaaclab.utils import configclass -from isaaclab_tasks.core.reorient.config.allegro_hand.allegro_hand_common import ( +from isaaclab_assets.robots.allegro import ALLEGRO_ACTUATED_JOINT_NAMES, ALLEGRO_FINGERTIP_BODY_NAMES + +from .allegro_hand_common import ( ALLEGRO_HAND_ROBOT_CFG, CUBE_CFG, GOAL_OBJECT_CFG, PhysicsCfg, ) -from isaaclab_assets.robots.allegro import ALLEGRO_ACTUATED_JOINT_NAMES, ALLEGRO_FINGERTIP_BODY_NAMES - @configclass class AllegroHandSceneCfg(InteractiveSceneCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py index 1314f7b360df..399057936677 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py @@ -10,17 +10,18 @@ from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass -import isaaclab_tasks.core.reorient.mdp as mdp -from isaaclab_tasks.core.reorient.config.allegro_hand.allegro_hand_common import ( +from isaaclab_tasks.utils import PresetCfg + +from isaaclab_assets.robots.allegro import ALLEGRO_ACTUATED_JOINT_NAMES, ALLEGRO_FINGERTIP_BODY_NAMES + +from ... import mdp +from ...reorient_manager_env_cfg import ReorientManagerEnvBaseCfg, ReorientSceneBaseCfg +from .allegro_hand_common import ( ALLEGRO_HAND_ROBOT_CFG, CUBE_CFG, GOAL_OBJECT_CFG, PhysicsCfg, ) -from isaaclab_tasks.core.reorient.reorient_manager_env_cfg import ReorientManagerEnvBaseCfg, ReorientSceneBaseCfg -from isaaclab_tasks.utils import PresetCfg - -from isaaclab_assets.robots.allegro import ALLEGRO_ACTUATED_JOINT_NAMES, ALLEGRO_FINGERTIP_BODY_NAMES @configclass diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py index e19235951879..7d3ac6f0e358 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py @@ -11,20 +11,21 @@ from isaaclab.utils import configclass from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR -import isaaclab_tasks.core.reorient.mdp as mdp -from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractorCfg -from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_camera_env_cfg import ( +from isaaclab_tasks.utils import preset + +from isaaclab_assets.robots.shadow_hand import FINGERTIP_NAMES + +from ... import mdp +from ...reorient_manager_env_cfg import ReorientFullStateObsCfg, ReorientRobotObsCfg +from .feature_extractor import FeatureExtractorCfg +from .shadow_hand_direct_camera_env_cfg import ( ShadowHandTiledCameraCfg, validate_shadow_hand_camera_settings, ) -from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_manager_env_cfg import ( +from .shadow_hand_manager_env_cfg import ( ShadowHandManagerEnvCfg, ShadowHandManagerSceneCfg, ) -from isaaclab_tasks.core.reorient.reorient_manager_env_cfg import ReorientFullStateObsCfg, ReorientRobotObsCfg -from isaaclab_tasks.utils import preset - -from isaaclab_assets.robots.shadow_hand import FINGERTIP_NAMES _PRETRAINED_CHECKPOINT_DIR = f"{ISAACLAB_NUCLEUS_DIR}/PretrainedCheckpoints/rsl_rl" _MANAGER_NEWTON_FEATURE_EXTRACTOR_CHECKPOINT = ( diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py index 6241fdd9a950..dd827fc3c71d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py @@ -24,11 +24,12 @@ from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR -import isaaclab_tasks.core.reorient.mdp as mdp from isaaclab_tasks.utils import PresetCfg from isaaclab_assets.robots.shadow_hand import SHADOW_HAND_NEWTON_CFG, SHADOW_HAND_PHYSX_CFG +from ... import mdp + @configclass class ShadowHandRandomizationEventCfg: diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py index 9bd29f3de7c5..a3b8475edc9e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py @@ -13,12 +13,12 @@ from isaaclab.utils.math import scale_transform -from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractor -from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_env import ShadowHandDirectEnv -from isaaclab_tasks.core.reorient.mdp.observations import compute_cube_keypoints +from ...mdp.observations import compute_cube_keypoints +from .feature_extractor import FeatureExtractor +from .shadow_hand_direct_env import ShadowHandDirectEnv if TYPE_CHECKING: - from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_camera_env_cfg import ShadowHandCameraEnvCfg + from .shadow_hand_direct_camera_env_cfg import ShadowHandCameraEnvCfg class ShadowHandCameraEnv(ShadowHandDirectEnv): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py index 9cd43663e64a..2aebac2ff33c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py @@ -13,13 +13,14 @@ from isaaclab.utils import configclass from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR -from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractorCfg -from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_env_cfg import ( +from isaaclab_tasks.utils import PresetCfg, preset +from isaaclab_tasks.utils.presets import MultiBackendRendererCfg + +from .feature_extractor import FeatureExtractorCfg +from .shadow_hand_direct_env_cfg import ( ShadowHandEnvCfg, ShadowHandSceneCfg, ) -from isaaclab_tasks.utils import PresetCfg, preset -from isaaclab_tasks.utils.presets import MultiBackendRendererCfg _PRETRAINED_CHECKPOINT_DIR = f"{ISAACLAB_NUCLEUS_DIR}/PretrainedCheckpoints/rsl_rl" _DIRECT_NEWTON_FEATURE_EXTRACTOR_CHECKPOINT = ( diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env.py index 752e6d5d908c..281d42d2f114 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env.py @@ -9,11 +9,11 @@ from isaaclab.utils.math import saturate, unscale_transform -from isaaclab_tasks.core.reorient.reorient_direct_env import ReorientDirectEnv -from isaaclab_tasks.core.reorient.utils import resolve_actuated_tendons +from ...reorient_direct_env import ReorientDirectEnv +from ...utils import resolve_actuated_tendons if TYPE_CHECKING: - from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_env_cfg import ShadowHandEnvCfg + from .shadow_hand_direct_env_cfg import ShadowHandEnvCfg class ShadowHandDirectEnv(ReorientDirectEnv): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env_cfg.py index 76cb3bee5ba8..9ffd0b5c4d00 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env_cfg.py @@ -15,15 +15,15 @@ from isaaclab.sim.spawners.materials import RigidBodyMaterialBaseCfg from isaaclab.utils import configclass -from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_common import ( +from isaaclab_assets.robots.shadow_hand import FINGERTIP_NAMES, JOINT_NAMES, TENDON_NAMES, TENDON_POSITION_LIMITS + +from .shadow_hand_common import ( CUBE_CFG, GOAL_OBJECT_CFG, PhysicsCfg, ShadowHandRobotCfg, ) -from isaaclab_assets.robots.shadow_hand import FINGERTIP_NAMES, JOINT_NAMES, TENDON_NAMES, TENDON_POSITION_LIMITS - @configclass class ShadowHandSceneCfg(InteractiveSceneCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py index d1e03e163222..d49fefd78169 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py @@ -12,23 +12,24 @@ from isaaclab.sensors import JointWrenchSensorCfg from isaaclab.utils import configclass -import isaaclab_tasks.core.reorient.mdp as mdp -from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_common import ( +from isaaclab_tasks.utils import PresetCfg + +from isaaclab_assets.robots.shadow_hand import FINGERTIP_NAMES, JOINT_NAMES, TENDON_NAMES, TENDON_POSITION_LIMITS + +from ... import mdp +from ...reorient_manager_env_cfg import ( + ActionsCfg, + ReorientFullStateObsCfg, + ReorientManagerEnvBaseCfg, + ReorientSceneBaseCfg, +) +from .shadow_hand_common import ( CUBE_CFG, GOAL_OBJECT_CFG, PhysicsCfg, ShadowHandManagerEventPresetCfg, ShadowHandRobotCfg, ) -from isaaclab_tasks.core.reorient.reorient_manager_env_cfg import ( - ActionsCfg, - ReorientFullStateObsCfg, - ReorientManagerEnvBaseCfg, - ReorientSceneBaseCfg, -) -from isaaclab_tasks.utils import PresetCfg - -from isaaclab_assets.robots.shadow_hand import FINGERTIP_NAMES, JOINT_NAMES, TENDON_NAMES, TENDON_POSITION_LIMITS ## # Full-state actor (default) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py index ee9f8609a319..7b464e3075a0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py @@ -17,7 +17,7 @@ from isaaclab.markers import VisualizationMarkers from isaaclab.utils.leapp import POSE7_ELEMENT_NAMES -from isaaclab_tasks.core.reorient.utils import SuccessTracker +from ..utils import SuccessTracker if TYPE_CHECKING: from isaaclab.assets import RigidObject diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/events.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/events.py index 325268500af2..bf1bc3633571 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/events.py @@ -13,7 +13,7 @@ import isaaclab.utils.math as math_utils from isaaclab.managers import SceneEntityCfg -from isaaclab_tasks.core.reorient.utils import sample_joint_positions_within_limits +from ..utils import sample_joint_positions_within_limits if TYPE_CHECKING: from isaaclab.assets import Articulation diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py index a1b1ac7958e7..d3c0f7562918 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py @@ -22,8 +22,7 @@ from isaaclab.envs import ManagerBasedRLEnv from isaaclab.sensors import Camera - from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractorCfg - + from ..config.shadow_hand.feature_extractor import FeatureExtractorCfg from .commands import ReorientCommand @@ -159,7 +158,7 @@ def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): sensor_cfg: SceneEntityCfg = cfg.params["sensor_cfg"] camera: Camera = env.scene.sensors[sensor_cfg.name] # deferred import: the config modules import this mdp package at module load - from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractor # noqa: PLC0415 + from ..config.shadow_hand.feature_extractor import FeatureExtractor feature_extractor_cfg: FeatureExtractorCfg = cfg.params["feature_extractor_cfg"] self._feature_extractor = FeatureExtractor( @@ -233,7 +232,8 @@ def __call__( def _cube_corner_offsets(size: tuple[float, float, float], num_keypoints: int, device: str) -> torch.Tensor: """Corner offsets [m] from the cube center; corner index bits select the +/- half side per axis. - Cached per ``(size, num_keypoints, device)`` so the hot path does not rebuild the constant every step. + Cached per ``(size, num_keypoints, device)`` so the hot path does not rebuild the constant every step; + ``device`` is a string so that it is hashable for the cache. """ signs = torch.tensor( [[1 - 2 * ((corner >> axis) & 1) for axis in range(3)] for corner in range(num_keypoints)], diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py index 7a1dd5167f77..fc86d297e7b3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py @@ -23,15 +23,15 @@ unscale_transform, ) -from isaaclab_tasks.core.reorient.utils import ( +from .utils import ( EpisodeErrorRecorder, randomize_rotation, sample_joint_positions_within_limits, ) if TYPE_CHECKING: - from isaaclab_tasks.core.reorient.config.allegro_hand.allegro_hand_direct_env_cfg import AllegroHandEnvCfg - from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_env_cfg import ShadowHandEnvCfg + from .config.allegro_hand.allegro_hand_direct_env_cfg import AllegroHandEnvCfg + from .config.shadow_hand.shadow_hand_direct_env_cfg import ShadowHandEnvCfg class ReorientDirectEnv(DirectRLEnv): @@ -302,10 +302,9 @@ def _reset_target_pose(self, env_ids: Sequence[int] | torch.Tensor) -> None: def _compute_intermediate_values(self) -> None: """Refresh the torch-side state snapshots consumed by the observation and reward paths.""" + env_origins = self.scene.env_origins.unsqueeze(1) # data for hand - self.fingertip_pos = self.hand.data.body_pos_w.torch[:, self.finger_bodies] - self.scene.env_origins.unsqueeze( - 1 - ) + self.fingertip_pos = self.hand.data.body_pos_w.torch[:, self.finger_bodies] - env_origins self.fingertip_rot = self.hand.data.body_quat_w.torch[:, self.finger_bodies] self.fingertip_velocities = self.hand.data.body_vel_w.torch[:, self.finger_bodies] @@ -408,9 +407,8 @@ def reorient_reward( ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Compute the Direct reorientation reward and success state transition. - The success evaluation is not recomputed here: callers pass the flags and - orientation errors computed once - per step. + The success evaluation is not recomputed here: callers pass the flags and orientation errors + computed once per step. Args: reset_buf: Current episode-reset flags. diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_manager_env_cfg.py index 9bb98c4475be..9ca1e940481e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_manager_env_cfg.py @@ -27,9 +27,10 @@ from isaaclab.utils import configclass from isaaclab.visualizers import VisualizerCfg -import isaaclab_tasks.core.reorient.mdp as mdp from isaaclab_tasks.utils import PresetCfg +from . import mdp + ## # Scene definition ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/rough_env_cfg.py index b50e6a5a2058..33ddda354f29 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/anymal_d/rough_env_cfg.py @@ -7,10 +7,10 @@ from isaaclab.utils import configclass -from isaaclab_tasks.core.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg - from isaaclab_assets.robots.anymal import ANYMAL_D_CFG +from ...velocity_env_cfg import LocomotionVelocityRoughEnvCfg + @configclass class AnymalDRoughEnvCfg(LocomotionVelocityRoughEnvCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/rough_env_cfg.py index d76d168f678e..25540be04a51 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/cassie/rough_env_cfg.py @@ -9,14 +9,14 @@ from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass -import isaaclab_tasks.core.velocity.mdp as mdp -from isaaclab_tasks.core.velocity.velocity_env_cfg import ( +from isaaclab_assets.robots.cassie import CASSIE_CFG + +from ... import mdp +from ...velocity_env_cfg import ( LocomotionVelocityRoughEnvCfg, RewardsCfg, ) -from isaaclab_assets.robots.cassie import CASSIE_CFG - @configclass class CassieRewardsCfg(RewardsCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/rough_env_cfg.py index affd57efde7b..617b4d185708 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/g1/rough_env_cfg.py @@ -9,14 +9,14 @@ from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass -import isaaclab_tasks.core.velocity.mdp as mdp -from isaaclab_tasks.core.velocity.velocity_env_cfg import ( +from isaaclab_assets import G1_MINIMAL_CFG + +from ... import mdp +from ...velocity_env_cfg import ( LocomotionVelocityRoughEnvCfg, RewardsCfg, ) -from isaaclab_assets import G1_MINIMAL_CFG - @configclass class G1Rewards(RewardsCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/rough_env_cfg.py index f61ecf82b80b..591b5cf51f25 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/go2/rough_env_cfg.py @@ -7,10 +7,10 @@ from isaaclab.utils import configclass -from isaaclab_tasks.core.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg - from isaaclab_assets.robots.unitree import UNITREE_GO2_CFG +from ...velocity_env_cfg import LocomotionVelocityRoughEnvCfg + @configclass class UnitreeGo2RoughEnvCfg(LocomotionVelocityRoughEnvCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/rough_env_cfg.py index 7a757832c46b..61bfb71f86d3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/config/h1/rough_env_cfg.py @@ -9,14 +9,14 @@ from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass -import isaaclab_tasks.core.velocity.mdp as mdp -from isaaclab_tasks.core.velocity.velocity_env_cfg import ( +from isaaclab_assets import H1_MINIMAL_CFG + +from ... import mdp +from ...velocity_env_cfg import ( LocomotionVelocityRoughEnvCfg, RewardsCfg, ) -from isaaclab_assets import H1_MINIMAL_CFG - @configclass class H1Rewards(RewardsCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/velocity_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/velocity_env_cfg.py index e86affbfe86e..ec1d215d5599 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/velocity/velocity_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/velocity/velocity_env_cfg.py @@ -38,9 +38,10 @@ from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR from isaaclab.utils.noise import UniformNoiseCfg as Unoise -import isaaclab_tasks.core.velocity.mdp as mdp from isaaclab_tasks.utils import PresetCfg +from . import mdp + ## # Physics backend presets ## diff --git a/source/isaaclab_tasks/isaaclab_tasks/utils/hydra.py b/source/isaaclab_tasks/isaaclab_tasks/utils/hydra.py index 61637f9a410f..e948f157e515 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/utils/hydra.py +++ b/source/isaaclab_tasks/isaaclab_tasks/utils/hydra.py @@ -45,7 +45,7 @@ _LITERAL_MAP = {"true": True, "false": False, "none": None, "null": None} -def _user_stacklevel() -> int: +def user_stacklevel() -> int: """Compute a ``warnings.warn`` stacklevel that lands on the first frame outside the ``isaaclab_tasks.utils`` package, so deprecation messages cite user code rather than internal utility frames. @@ -92,7 +92,7 @@ def _normalize_preset_name(name: str, known_names: set[str]) -> str: warnings.warn( f"Preset '{name}' is deprecated. Use '{replacement}' instead.", FutureWarning, - stacklevel=_user_stacklevel(), + stacklevel=user_stacklevel(), ) return replacement @@ -148,7 +148,7 @@ def __getattr__(self, name: str): warnings.warn( f"Preset '{name}' is deprecated. Use '{replacement}' instead.", FutureWarning, - stacklevel=_user_stacklevel(), + stacklevel=user_stacklevel(), ) return getattr(self, replacement) raise AttributeError(f"{type(self).__name__!s} object has no attribute {name!r}") diff --git a/source/isaaclab_tasks/isaaclab_tasks/utils/parse_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/utils/parse_cfg.py index 8e7c4126f7fe..cfbf2c85798e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/utils/parse_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/utils/parse_cfg.py @@ -19,7 +19,7 @@ import gymnasium as gym import yaml -from isaaclab_tasks.utils.hydra import _user_stacklevel, resolve_task_config +from isaaclab_tasks.utils.hydra import resolve_task_config, user_stacklevel if TYPE_CHECKING: from isaaclab.envs import DirectRLEnvCfg, ManagerBasedRLEnvCfg @@ -80,7 +80,7 @@ def load_cfg_from_registry(task_name: str, entry_point_key: str) -> dict | objec warnings.warn( f"Task '{spec.id}' is deprecated and will be removed in a future release. Use '{new_command}'.", FutureWarning, - stacklevel=_user_stacklevel(), + stacklevel=user_stacklevel(), ) # obtain the configuration entry point cfg_entry_point = spec.kwargs.get(entry_point_key) diff --git a/source/isaaclab_tasks/test/contrib/test_franka_pour_env_cfg.py b/source/isaaclab_tasks/test/contrib/test_franka_pour_env_cfg.py index 37e4f5fd78dd..093e7551d56b 100644 --- a/source/isaaclab_tasks/test/contrib/test_franka_pour_env_cfg.py +++ b/source/isaaclab_tasks/test/contrib/test_franka_pour_env_cfg.py @@ -16,9 +16,9 @@ _MEDIA_FILL_RESOLUTION, FRANKA_POUR_ROBOT_ASSET_ID, FrankaPourResetDatasetEnvCfg, - _configure_mpm_capacities, _reset_dataset_task_contract, _resolve_pour_solver_tree, + configure_mpm_capacities, ) @@ -50,7 +50,7 @@ def test_source_fill_level_controls_height_and_particle_count(): cfg.source_fill_level = 0.50 cfg.scene.num_envs = 1 - _configure_mpm_capacities(cfg) + configure_mpm_capacities(cfg) assert cfg.scene.media is media assert media_particle_count(media) == 7 * 7 * 11 @@ -61,7 +61,7 @@ def test_source_fill_level_controls_height_and_particle_count(): ) cfg.source_fill_level = 1.0 - _configure_mpm_capacities(cfg) + configure_mpm_capacities(cfg) assert media_particle_count(media) == 7 * 7 * 21 @@ -123,17 +123,17 @@ def test_capacity_resolution_only_updates_world_dependent_solver_limits(): solver = _resolve_pour_solver_tree(cfg).media_solver cfg.scene.num_envs = 1 - _configure_mpm_capacities(cfg) + configure_mpm_capacities(cfg) assert solver.max_active_cell_count == 1024 assert (solver.max_leaf_node_count, solver.max_lower_node_count, solver.max_upper_node_count) == (-1, -1, -1) cfg.scene.num_envs = 7 - _configure_mpm_capacities(cfg) + configure_mpm_capacities(cfg) assert solver.max_active_cell_count == 7168 assert (solver.max_leaf_node_count, solver.max_lower_node_count, solver.max_upper_node_count) == (-1, -1, -1) cfg.mpm_cell_cap_override = 16 - _configure_mpm_capacities(cfg) + configure_mpm_capacities(cfg) assert solver.max_active_cell_count == 16 assert (solver.max_leaf_node_count, solver.max_lower_node_count, solver.max_upper_node_count) == (-1, -1, -1) assert cfg.scene.source_cup is source_cup diff --git a/source/isaaclab_tasks/test/contrib/test_franka_pour_reset_sampler.py b/source/isaaclab_tasks/test/contrib/test_franka_pour_reset_sampler.py index 33e42681b12f..3c22bf2a48bf 100644 --- a/source/isaaclab_tasks/test/contrib/test_franka_pour_reset_sampler.py +++ b/source/isaaclab_tasks/test/contrib/test_franka_pour_reset_sampler.py @@ -10,10 +10,10 @@ import pytest import torch -from isaaclab_tasks.contrib.franka_pour.reset_sampler import ResetDatasetSamplerCfg, _ResetDatasetSampler +from isaaclab_tasks.contrib.franka_pour.reset_sampler import ResetDatasetSampler, ResetDatasetSamplerCfg -def _ring_values(sampler: _ResetDatasetSampler, row: int) -> list[bool]: +def _ring_values(sampler: ResetDatasetSampler, row: int) -> list[bool]: """Read one sampler ring in oldest-to-newest order.""" size = int(sampler._history_sizes[row]) pointer = int(sampler._history_pointers[row]) @@ -26,7 +26,7 @@ def test_rolling_history_matches_per_row_deques(): """Duplicate IDs, wraparound, and oversized batches match sequential deque updates.""" capacity = 5 row_count = 4 - sampler = _ResetDatasetSampler( + sampler = ResetDatasetSampler( row_count, "cpu", ResetDatasetSamplerCfg(monitored_history_len=capacity), @@ -53,7 +53,7 @@ def test_rolling_history_matches_per_row_deques(): def test_uniform_replay_covers_every_row_before_repeating(monkeypatch: pytest.MonkeyPatch): """The configured replay fraction draws one complete shuffled cycle without replacement.""" - sampler = _ResetDatasetSampler( + sampler = ResetDatasetSampler( 5, "cpu", ResetDatasetSamplerCfg(uniform_fraction=0.5), diff --git a/source/isaaclab_tasks/test/core/test_hydra.py b/source/isaaclab_tasks/test/core/test_hydra.py index 10fd5e1807c5..0101307e10e2 100644 --- a/source/isaaclab_tasks/test/core/test_hydra.py +++ b/source/isaaclab_tasks/test/core/test_hydra.py @@ -427,7 +427,7 @@ def test_user_stacklevel_warning_origin_is_outside_hydra_module(): deprecations = [w for w in caught if issubclass(w.category, FutureWarning)] assert deprecations, "expected a FutureWarning from the legacy alias" assert deprecations[0].filename != hydra_mod.__file__, ( - f"warning was attributed to hydra.py ({deprecations[0].filename}); _user_stacklevel should " + f"warning was attributed to hydra.py ({deprecations[0].filename}); user_stacklevel should " f"point outside the module" )