diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 92f854ac96..b7873872c8 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -2,12 +2,29 @@ name: Build & deploy docs on: push: - pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: + check-secrets: + name: Check secrets + runs-on: ubuntu-latest + outputs: + trigger-deploy: ${{ steps.trigger-deploy.outputs.defined }} + steps: + - id: trigger-deploy + env: + REPO_NAME: ${{ secrets.REPO_NAME }} + BRANCH_REF: ${{ secrets.BRANCH_REF }} + if: "${{ github.repository == env.REPO_NAME && github.ref == env.BRANCH_REF }}" + run: echo "defined=true" >> "$GITHUB_OUTPUT" + build-docs: name: Build Docs runs-on: ubuntu-latest + needs: [check-secrets] steps: - name: Checkout code @@ -24,8 +41,8 @@ jobs: run: pip install -r requirements.txt - name: Check branch docs building - if: ${{ github.event_name == 'pull_request' }} working-directory: ./docs + if: needs.check-secrets.outputs.trigger-deploy != 'true' run: make current-docs - name: Generate multi-version docs @@ -40,19 +57,6 @@ jobs: name: docs-html path: ./docs/_build - check-secrets: - name: Check secrets - runs-on: ubuntu-latest - outputs: - trigger-deploy: ${{ steps.trigger-deploy.outputs.defined }} - steps: - - id: trigger-deploy - env: - REPO_NAME: ${{ secrets.REPO_NAME }} - BRANCH_REF: ${{ secrets.BRANCH_REF }} - if: "${{ github.repository == env.REPO_NAME && github.ref == env.BRANCH_REF }}" - run: echo "defined=true" >> "$GITHUB_OUTPUT" - deploy-docs: name: Deploy Docs runs-on: ubuntu-latest diff --git a/docs/conf.py b/docs/conf.py index 4c7a777559..0fccd611de 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -262,7 +262,7 @@ def skip_member(app, what, name, obj, skip, options): # List the names of the functions you want to skip here - exclusions = ["from_dict", "to_dict", "replace", "copy", "__post_init__"] + exclusions = ["from_dict", "to_dict", "replace", "copy", "validate", "__post_init__"] if name in exclusions: return True return None diff --git a/docs/source/tutorials/03_envs/create_manager_rl_env.rst b/docs/source/tutorials/03_envs/create_manager_rl_env.rst index 1ff7c71990..63f710965b 100644 --- a/docs/source/tutorials/03_envs/create_manager_rl_env.rst +++ b/docs/source/tutorials/03_envs/create_manager_rl_env.rst @@ -36,7 +36,7 @@ For this tutorial, we use the cartpole environment defined in ``omni.isaac.lab_t .. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py :language: python - :emphasize-lines: 63-68, 124-149, 152-162, 165-169, 187-192 + :emphasize-lines: 117-141, 144-154, 172-174 :linenos: The script for running the environment ``run_cartpole_rl_env.py`` is present in the @@ -117,13 +117,8 @@ For various goal-conditioned tasks, it is useful to specify the goals or command handled through the :class:`managers.CommandManager`. The command manager handles resampling and updating the commands at each step. It can also be used to provide the commands as an observation to the agent. -For this simple task, we do not use any commands. This is specified by using a command term with the -:class:`envs.mdp.NullCommandCfg` configuration. However, you can see an example of command definitions in the -locomotion or manipulation tasks. - -.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py - :language: python - :pyobject: CommandsCfg +For this simple task, we do not use any commands. Hence, we leave this attribute as its default value, which is None. +You can see an example of how to define a command manager in the other locomotion or manipulation tasks. Defining curriculum ------------------- @@ -134,11 +129,6 @@ we provide a :class:`managers.CurriculumManager` class that can be used to defin In this tutorial we don't implement a curriculum for simplicity, but you can see an example of a curriculum definition in the other locomotion or manipulation tasks. -We use a simple pass-through curriculum to define a curriculum manager that does not modify the environment. - -.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py - :language: python - :pyobject: CurriculumCfg Tying it all together --------------------- diff --git a/source/extensions/omni.isaac.lab/config/extension.toml b/source/extensions/omni.isaac.lab/config/extension.toml index 8252683087..db8ec4d6b1 100644 --- a/source/extensions/omni.isaac.lab/config/extension.toml +++ b/source/extensions/omni.isaac.lab/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.26.0" +version = "0.27.0" # Description title = "Isaac Lab framework for Robot Learning" diff --git a/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst b/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst index 433db0cd8c..ab1a562b1e 100644 --- a/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst +++ b/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst @@ -1,6 +1,23 @@ Changelog --------- +0.27.0 (2024-10-14) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added a method to :class:`~omni.isaac.lab.utils.configclass` to check for attributes with values of + type ``MISSING``. This is useful when the user wants to check if a certain attribute has been set or not. +* Added the configuration validation check inside the constructor of all the core classes + (such as sensor base, asset base, scene and environment base classes). +* Added support for environments without commands by leaving the attribute + :attr:`omni.isaac.lab.envs.ManagerBasedRLEnvCfg.commands` as None. Before, this had to be done using + the class :class:`omni.isaac.lab.command_generators.NullCommandGenerator`. +* Moved the ``meshes`` attribute in the :class:`omni.isaac.lab.sensors.RayCaster` class from class variable to instance variable. + This prevents the meshes to overwrite each other. + + 0.26.0 (2024-10-16) ~~~~~~~~~~~~~~~~~~~ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/asset_base.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/asset_base.py index 9a55a87ef0..8c66bb626a 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/asset_base.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/asset_base.py @@ -59,6 +59,8 @@ def __init__(self, cfg: AssetBaseCfg): Raises: RuntimeError: If no prims found at input prim path or prim path expression. """ + # check that the config is valid + cfg.validate() # store inputs self.cfg = cfg # flag for whether the asset is initialized diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/asset_base_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/asset_base_cfg.py index 6bea572dcb..62d047fb5c 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/asset_base_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/asset_base_cfg.py @@ -39,8 +39,9 @@ class InitialStateCfg: Defaults to (1.0, 0.0, 0.0, 0.0). """ - class_type: type[AssetBase] = MISSING - """The associated asset class. + class_type: type[AssetBase] = None + """The associated asset class. Defaults to None, which means that the asset will be spawned + but cannot be interacted with via the asset class. The class should inherit from :class:`omni.isaac.lab.assets.asset_base.AssetBase`. """ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_marl_env.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_marl_env.py index 5bcedb0591..70c0b3f662 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_marl_env.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_marl_env.py @@ -74,6 +74,8 @@ def __init__(self, cfg: DirectMARLEnvCfg, render_mode: str | None = None, **kwar RuntimeError: If a simulation context already exists. The environment must always create one since it configures the simulation context and controls the simulation. """ + # check that the config is valid + cfg.validate() # store inputs to class self.cfg = cfg # store the render mode diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_rl_env.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_rl_env.py index 582d8ad155..32dafdef33 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_rl_env.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_rl_env.py @@ -79,6 +79,8 @@ def __init__(self, cfg: DirectRLEnvCfg, render_mode: str | None = None, **kwargs RuntimeError: If a simulation context already exists. The environment must always create one since it configures the simulation context and controls the simulation. """ + # check that the config is valid + cfg.validate() # store inputs to class self.cfg = cfg # store the render mode diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_rl_env_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_rl_env_cfg.py index 888db7c56a..4e4f1725c8 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_rl_env_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_rl_env_cfg.py @@ -98,7 +98,7 @@ class DirectRLEnvCfg: Please refer to the :class:`omni.isaac.lab.scene.InteractiveSceneCfg` class for more details. """ - events: object = None + events: object | None = None """Event settings. Defaults to None, in which case no events are applied through the event manager. Please refer to the :class:`omni.isaac.lab.managers.EventManager` class for more details. diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_env.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_env.py index 9b2991a521..ccf8a5ae14 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_env.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_env.py @@ -69,6 +69,8 @@ def __init__(self, cfg: ManagerBasedEnvCfg): RuntimeError: If a simulation context already exists. The environment must always create one since it configures the simulation context and controls the simulation. """ + # check that the config is valid + cfg.validate() # store inputs to class self.cfg = cfg # initialize internal variables diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_rl_env_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_rl_env_cfg.py index b0def63606..93195d4d55 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_rl_env_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_rl_env_cfg.py @@ -67,14 +67,14 @@ class ManagerBasedRLEnvCfg(ManagerBasedEnvCfg): Please refer to the :class:`omni.isaac.lab.managers.TerminationManager` class for more details. """ - curriculum: object = MISSING - """Curriculum settings. + curriculum: object | None = None + """Curriculum settings. Defaults to None, in which case no curriculum is applied. Please refer to the :class:`omni.isaac.lab.managers.CurriculumManager` class for more details. """ - commands: object = MISSING - """Command settings. + commands: object | None = None + """Command settings. Defaults to None, in which case no commands are generated. Please refer to the :class:`omni.isaac.lab.managers.CommandManager` class for more details. """ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/joint_actions_to_limits.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/joint_actions_to_limits.py index 6478f9c82c..3b31c9502a 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/joint_actions_to_limits.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/joint_actions_to_limits.py @@ -183,9 +183,11 @@ def reset(self, env_ids: Sequence[int] | None = None) -> None: # check if specific environment ids are provided if env_ids is None: env_ids = slice(None) + else: + env_ids = env_ids[:, None] super().reset(env_ids) # reset history to current joint positions - self._prev_applied_actions[env_ids, :] = self._asset.data.joint_pos[env_ids[:, None], self._joint_ids] + self._prev_applied_actions[env_ids, :] = self._asset.data.joint_pos[env_ids, self._joint_ids] def process_actions(self, actions: torch.Tensor): # apply affine transformations diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/commands/commands_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/commands/commands_cfg.py index d548f554db..d19bea60e2 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/commands/commands_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/commands/commands_cfg.py @@ -37,29 +37,46 @@ class UniformVelocityCommandCfg(CommandTermCfg): asset_name: str = MISSING """Name of the asset in the environment for which the commands are generated.""" - heading_command: bool = MISSING - """Whether to use heading command or angular velocity command. + + heading_command: bool = False + """Whether to use heading command or angular velocity command. Defaults to False. If True, the angular velocity command is computed from the heading error, where the target heading is sampled uniformly from provided range. Otherwise, the angular velocity command is sampled uniformly from provided range. """ - heading_control_stiffness: float = MISSING - """Scale factor to convert the heading error to angular velocity command.""" - rel_standing_envs: float = MISSING - """Probability threshold for environments where the robots that are standing still.""" - rel_heading_envs: float = MISSING - """Probability threshold for environments where the robots follow the heading-based angular velocity command - (the others follow the sampled angular velocity command).""" + + heading_control_stiffness: float = 1.0 + """Scale factor to convert the heading error to angular velocity command. Defaults to 1.0.""" + + rel_standing_envs: float = 0.0 + """The sampled probability of environments that should be standing still. Defaults to 0.0.""" + + rel_heading_envs: float = 1.0 + """The sampled probability of environments where the robots follow the heading-based angular velocity command + (the others follow the sampled angular velocity command). Defaults to 1.0. + + This parameter is only used if :attr:`heading_command` is True. + """ @configclass class Ranges: """Uniform distribution ranges for the velocity commands.""" - lin_vel_x: tuple[float, float] = MISSING # min max [m/s] - lin_vel_y: tuple[float, float] = MISSING # min max [m/s] - ang_vel_z: tuple[float, float] = MISSING # min max [rad/s] - heading: tuple[float, float] = MISSING # min max [rad] + lin_vel_x: tuple[float, float] = MISSING + """Range for the linear-x velocity command (in m/s).""" + + lin_vel_y: tuple[float, float] = MISSING + """Range for the linear-y velocity command (in m/s).""" + + ang_vel_z: tuple[float, float] = MISSING + """Range for the angular-z velocity command (in rad/s).""" + + heading: tuple[float, float] | None = None + """Range for the heading command (in rad). Defaults to None. + + This parameter is only used if :attr:`~UniformVelocityCommandCfg.heading_command` is True. + """ ranges: Ranges = MISSING """Distribution ranges for the velocity commands.""" @@ -91,15 +108,17 @@ class Ranges: """Normal distribution ranges for the velocity commands.""" mean_vel: tuple[float, float, float] = MISSING - """Mean velocity for the normal distribution. + """Mean velocity for the normal distribution (in m/s). The tuple contains the mean linear-x, linear-y, and angular-z velocity. """ + std_vel: tuple[float, float, float] = MISSING - """Standard deviation for the normal distribution. + """Standard deviation for the normal distribution (in m/s). The tuple contains the standard deviation linear-x, linear-y, and angular-z velocity. """ + zero_prob: tuple[float, float, float] = MISSING """Probability of zero velocity for the normal distribution. @@ -118,6 +137,7 @@ class UniformPoseCommandCfg(CommandTermCfg): asset_name: str = MISSING """Name of the asset in the environment for which the commands are generated.""" + body_name: str = MISSING """Name of the body in the asset for which the commands are generated.""" @@ -131,12 +151,23 @@ class UniformPoseCommandCfg(CommandTermCfg): class Ranges: """Uniform distribution ranges for the pose commands.""" - pos_x: tuple[float, float] = MISSING # min max [m] - pos_y: tuple[float, float] = MISSING # min max [m] - pos_z: tuple[float, float] = MISSING # min max [m] - roll: tuple[float, float] = MISSING # min max [rad] - pitch: tuple[float, float] = MISSING # min max [rad] - yaw: tuple[float, float] = MISSING # min max [rad] + pos_x: tuple[float, float] = MISSING + """Range for the x position (in m).""" + + pos_y: tuple[float, float] = MISSING + """Range for the y position (in m).""" + + pos_z: tuple[float, float] = MISSING + """Range for the z position (in m).""" + + roll: tuple[float, float] = MISSING + """Range for the roll angle (in rad).""" + + pitch: tuple[float, float] = MISSING + """Range for the pitch angle (in rad).""" + + yaw: tuple[float, float] = MISSING + """Range for the yaw angle (in rad).""" ranges: Ranges = MISSING """Ranges for the commands.""" @@ -175,8 +206,10 @@ class Ranges: pos_x: tuple[float, float] = MISSING """Range for the x position (in m).""" + pos_y: tuple[float, float] = MISSING """Range for the y position (in m).""" + heading: tuple[float, float] = MISSING """Heading range for the position commands (in rad). diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/commands/velocity_command.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/commands/velocity_command.py index 4a35adc5fd..2cabd86dba 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/commands/velocity_command.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/commands/velocity_command.py @@ -11,6 +11,8 @@ from collections.abc import Sequence from typing import TYPE_CHECKING +import omni.log + import omni.isaac.lab.utils.math as math_utils from omni.isaac.lab.assets import Articulation from omni.isaac.lab.managers import CommandTerm @@ -49,10 +51,25 @@ def __init__(self, cfg: UniformVelocityCommandCfg, env: ManagerBasedEnv): Args: cfg: The configuration of the command generator. env: The environment. + + Raises: + ValueError: If the heading command is active but the heading range is not provided. """ # initialize the base class super().__init__(cfg, env) + # check configuration + if self.cfg.heading_command and self.cfg.ranges.heading is None: + raise ValueError( + "The velocity command has heading commands active (heading_command=True) but the `ranges.heading`" + " parameter is set to None." + ) + if self.cfg.ranges.heading and not self.cfg.heading_command: + omni.log.warn( + f"The velocity command has the 'ranges.heading' attribute set to '{self.cfg.ranges.heading}'" + " but the heading command is not active. Consider setting the flag for the heading command to True." + ) + # obtain the robot asset # -- robot self.robot: Articulation = env.scene[cfg.asset_name] diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/action_manager.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/action_manager.py index 56a7ff92dc..2f729cde23 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/action_manager.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/action_manager.py @@ -181,12 +181,21 @@ def __init__(self, cfg: object, env: ManagerBasedEnv): Args: cfg: The configuration object or dictionary (``dict[str, ActionTermCfg]``). env: The environment instance. + + Raises: + ValueError: If the configuration is None. """ + # check if config is None + if cfg is None: + raise ValueError("Action manager configuration is None. Please provide a valid configuration.") + + # call the base class constructor (this prepares the terms) super().__init__(cfg, env) # create buffers to store actions self._action = torch.zeros((self.num_envs, self.total_action_dim), device=self.device) self._prev_action = torch.zeros_like(self._action) + # check if any term has debug visualization implemented self.cfg.debug_vis = False for term in self._terms.values(): self.cfg.debug_vis |= term.cfg.debug_vis @@ -334,8 +343,7 @@ def get_term(self, name: str) -> ActionTerm: """ def _prepare_terms(self): - """Prepares a list of action terms.""" - # parse action terms from the config + # create buffers to parse and store terms self._term_names: list[str] = list() self._terms: dict[str, ActionTerm] = dict() @@ -344,6 +352,7 @@ def _prepare_terms(self): cfg_items = self.cfg.items() else: cfg_items = self.cfg.__dict__.items() + # parse action terms from the config for term_name, term_cfg in cfg_items: # check if term config is None if term_cfg is None: diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/command_manager.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/command_manager.py index 5cf7e929ac..2b4451f7e4 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/command_manager.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/command_manager.py @@ -243,12 +243,17 @@ def __init__(self, cfg: object, env: ManagerBasedRLEnv): cfg: The configuration object or dictionary (``dict[str, CommandTermCfg]``). env: The environment instance. """ + # create buffers to parse and store terms + self._terms: dict[str, CommandTerm] = dict() + + # call the base class constructor (this prepares the terms) super().__init__(cfg, env) # store the commands self._commands = dict() - self.cfg.debug_vis = False - for term in self._terms.values(): - self.cfg.debug_vis |= term.cfg.debug_vis + if self.cfg: + self.cfg.debug_vis = False + for term in self._terms.values(): + self.cfg.debug_vis |= term.cfg.debug_vis def __str__(self) -> str: """Returns: A string representation for the command manager.""" @@ -371,10 +376,6 @@ def get_term(self, name: str) -> CommandTerm: """ def _prepare_terms(self): - """Prepares a list of command terms.""" - # parse command terms from the config - self._terms: dict[str, CommandTerm] = dict() - # check if config is dict already if isinstance(self.cfg, dict): cfg_items = self.cfg.items() diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/curriculum_manager.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/curriculum_manager.py index b9bef068bf..92fe7e7ef7 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/curriculum_manager.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/curriculum_manager.py @@ -44,7 +44,14 @@ def __init__(self, cfg: object, env: ManagerBasedRLEnv): TypeError: If curriculum term is not of type :class:`CurriculumTermCfg`. ValueError: If curriculum term configuration does not satisfy its function signature. """ + # create buffers to parse and store terms + self._term_names: list[str] = list() + self._term_cfgs: list[CurriculumTermCfg] = list() + self._class_term_cfgs: list[CurriculumTermCfg] = list() + + # call the base class constructor (this will parse the terms config) super().__init__(cfg, env) + # prepare logging self._curriculum_state = dict() for term_name in self._term_names: @@ -136,11 +143,6 @@ def compute(self, env_ids: Sequence[int] | None = None): """ def _prepare_terms(self): - # parse remaining curriculum terms and decimate their information - self._term_names: list[str] = list() - self._term_cfgs: list[CurriculumTermCfg] = list() - self._class_term_cfgs: list[CurriculumTermCfg] = list() - # check if config is dict already if isinstance(self.cfg, dict): cfg_items = self.cfg.items() diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/event_manager.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/event_manager.py index 9843164ba0..9209fe1d4a 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/event_manager.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/event_manager.py @@ -62,6 +62,12 @@ def __init__(self, cfg: object, env: ManagerBasedEnv): cfg: A configuration object or dictionary (``dict[str, EventTermCfg]``). env: An environment object. """ + # create buffers to parse and store terms + self._mode_term_names: dict[str, list[str]] = dict() + self._mode_term_cfgs: dict[str, list[EventTermCfg]] = dict() + self._mode_class_term_cfgs: dict[str, list[EventTermCfg]] = dict() + + # call the base class (this will parse the terms config) super().__init__(cfg, env) def __str__(self) -> str: @@ -294,11 +300,6 @@ def get_term_cfg(self, term_name: str) -> EventTermCfg: """ def _prepare_terms(self): - """Prepares a list of event functions.""" - # parse remaining event terms and decimate their information - self._mode_term_names: dict[str, list[str]] = dict() - self._mode_term_cfgs: dict[str, list[EventTermCfg]] = dict() - self._mode_class_term_cfgs: dict[str, list[EventTermCfg]] = dict() # buffer to store the time left for "interval" mode # if interval is global, then it is a single value, otherwise it is per environment self._interval_term_time_left: list[torch.Tensor] = list() diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/manager_base.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/manager_base.py index 2bc9236cab..4da002934f 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/manager_base.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/manager_base.py @@ -120,14 +120,15 @@ def __init__(self, cfg: object, env: ManagerBasedEnv): """Initialize the manager. Args: - cfg: The configuration object. + cfg: The configuration object. If None, the manager is initialized without any terms. env: The environment instance. """ # store the inputs self.cfg = copy.deepcopy(cfg) self._env = env # parse config to create terms information - self._prepare_terms() + if self.cfg: + self._prepare_terms() """ Properties. diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/observation_manager.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/observation_manager.py index 58ae0f55f3..1e0391b3a0 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/observation_manager.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/observation_manager.py @@ -63,9 +63,15 @@ def __init__(self, cfg: object, env: ManagerBasedEnv): env: The environment instance. Raises: + ValueError: If the configuration is None. RuntimeError: If the shapes of the observation terms in a group are not compatible for concatenation and the :attr:`~ObservationGroupCfg.concatenate_terms` attribute is set to True. """ + # check that cfg is not None + if cfg is None: + raise ValueError("Observation manager configuration is None. Please provide a valid configuration.") + + # call the base class constructor (this will parse the terms config) super().__init__(cfg, env) # compute combined vector for obs group diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/reward_manager.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/reward_manager.py index c10bc12ec5..5e17e0516e 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/reward_manager.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/reward_manager.py @@ -47,6 +47,12 @@ def __init__(self, cfg: object, env: ManagerBasedRLEnv): cfg: The configuration object or dictionary (``dict[str, RewardTermCfg]``). env: The environment instance. """ + # create buffers to parse and store terms + self._term_names: list[str] = list() + self._term_cfgs: list[RewardTermCfg] = list() + self._class_term_cfgs: list[RewardTermCfg] = list() + + # call the base class constructor (this will parse the terms config) super().__init__(cfg, env) # prepare extra info to store individual reward term information self._episode_sums = dict() @@ -185,12 +191,6 @@ def get_term_cfg(self, term_name: str) -> RewardTermCfg: """ def _prepare_terms(self): - """Prepares a list of reward functions.""" - # parse remaining reward terms and decimate their information - self._term_names: list[str] = list() - self._term_cfgs: list[RewardTermCfg] = list() - self._class_term_cfgs: list[RewardTermCfg] = list() - # check if config is dict already if isinstance(self.cfg, dict): cfg_items = self.cfg.items() diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/termination_manager.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/termination_manager.py index bbd1924048..77b32f2a53 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/termination_manager.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/termination_manager.py @@ -53,6 +53,12 @@ def __init__(self, cfg: object, env: ManagerBasedRLEnv): cfg: The configuration object or dictionary (``dict[str, TerminationTermCfg]``). env: An environment object. """ + # create buffers to parse and store terms + self._term_names: list[str] = list() + self._term_cfgs: list[TerminationTermCfg] = list() + self._class_term_cfgs: list[TerminationTermCfg] = list() + + # call the base class constructor (this will parse the terms config) super().__init__(cfg, env) # prepare extra info to store individual termination term information self._term_dones = dict() @@ -219,12 +225,6 @@ def get_term_cfg(self, term_name: str) -> TerminationTermCfg: """ def _prepare_terms(self): - """Prepares a list of termination functions.""" - # parse remaining termination terms and decimate their information - self._term_names: list[str] = list() - self._term_cfgs: list[TerminationTermCfg] = list() - self._class_term_cfgs: list[TerminationTermCfg] = list() - # check if config is dict already if isinstance(self.cfg, dict): cfg_items = self.cfg.items() diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/scene/interactive_scene.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/scene/interactive_scene.py index 85775ba425..0e74a9f878 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/scene/interactive_scene.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/scene/interactive_scene.py @@ -104,6 +104,8 @@ def __init__(self, cfg: InteractiveSceneCfg): Args: cfg: The configuration class for the scene. """ + # check that the config is valid + cfg.validate() # store inputs self.cfg = cfg # initialize scene elements diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/ray_caster/ray_caster.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/ray_caster/ray_caster.py index 8be96c358d..ec25f88f24 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/ray_caster/ray_caster.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/ray_caster/ray_caster.py @@ -9,7 +9,7 @@ import re import torch from collections.abc import Sequence -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING import omni.log import omni.physics.tensors.impl.api as physx @@ -48,14 +48,6 @@ class RayCaster(SensorBase): cfg: RayCasterCfg """The configuration parameters.""" - meshes: ClassVar[dict[str, wp.Mesh]] = {} - """The warp meshes available for raycasting. - - The keys correspond to the prim path for the meshes, and values are the corresponding warp Mesh objects. - - Note: - We store a global dictionary of all warp meshes to prevent re-loading the mesh for different ray-cast sensor instances. - """ def __init__(self, cfg: RayCasterCfg): """Initializes the ray-caster object. @@ -77,6 +69,8 @@ def __init__(self, cfg: RayCasterCfg): super().__init__(cfg) # Create empty variables for storing output data self._data = RayCasterData() + # the warp meshes used for raycasting. + self.meshes: dict[str, wp.Mesh] = {} def __str__(self) -> str: """Returns: A string containing information about the instance.""" @@ -84,7 +78,7 @@ def __str__(self) -> str: f"Ray-caster @ '{self.cfg.prim_path}': \n" f"\tview type : {self._view.__class__}\n" f"\tupdate period (s) : {self.cfg.update_period}\n" - f"\tnumber of meshes : {len(RayCaster.meshes)}\n" + f"\tnumber of meshes : {len(self.meshes)}\n" f"\tnumber of sensors : {self._view.count}\n" f"\tnumber of rays/sensor: {self.num_rays}\n" f"\ttotal number of rays : {self.num_rays * self._view.count}" @@ -163,10 +157,6 @@ def _initialize_warp_meshes(self): # read prims to ray-cast for mesh_prim_path in self.cfg.mesh_prim_paths: - # check if mesh already casted into warp mesh - if mesh_prim_path in RayCaster.meshes: - continue - # check if the prim is a plane - handle PhysX plane as a special case # if a plane exists then we need to create an infinite mesh that is a plane mesh_prim = sim_utils.get_first_matching_child_prim( @@ -197,10 +187,10 @@ def _initialize_warp_meshes(self): # print info omni.log.info(f"Created infinite plane mesh prim: {mesh_prim.GetPath()}.") # add the warp mesh to the list - RayCaster.meshes[mesh_prim_path] = wp_mesh + self.meshes[mesh_prim_path] = wp_mesh # throw an error if no meshes are found - if all([mesh_prim_path not in RayCaster.meshes for mesh_prim_path in self.cfg.mesh_prim_paths]): + if all([mesh_prim_path not in self.meshes for mesh_prim_path in self.cfg.mesh_prim_paths]): raise RuntimeError( f"No meshes found for ray-casting! Please check the mesh prim paths: {self.cfg.mesh_prim_paths}" ) @@ -263,7 +253,7 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): ray_starts_w, ray_directions_w, max_dist=self.cfg.max_distance, - mesh=RayCaster.meshes[self.cfg.mesh_prim_paths[0]], + mesh=self.meshes[self.cfg.mesh_prim_paths[0]], )[0] def _set_debug_vis_impl(self, debug_vis: bool): diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/ray_caster/ray_caster_camera.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/ray_caster/ray_caster_camera.py index 9a7e483bc1..03da4ca9b8 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/ray_caster/ray_caster_camera.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/ray_caster/ray_caster_camera.py @@ -281,7 +281,7 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): self.ray_hits_w, ray_depth, ray_normal, _ = raycast_mesh( ray_starts_w, ray_directions_w, - mesh=RayCasterCamera.meshes[self.cfg.mesh_prim_paths[0]], + mesh=self.meshes[self.cfg.mesh_prim_paths[0]], max_dist=1e6, return_distance=any( [name in self.cfg.data_types for name in ["distance_to_image_plane", "distance_to_camera"]] diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/sensor_base.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/sensor_base.py index 8f0d2d5092..b87e209d0a 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/sensor_base.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/sensor_base.py @@ -48,6 +48,8 @@ def __init__(self, cfg: SensorBaseCfg): # check that config is valid if cfg.history_length < 0: raise ValueError(f"History length must be greater than 0! Received: {cfg.history_length}") + # check that the config is valid + cfg.validate() # store inputs self.cfg = cfg # flag for whether the sensor is initialized diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/simulation_context.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/simulation_context.py index 315ad18d47..ce1278f5fc 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/simulation_context.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/simulation_context.py @@ -114,6 +114,8 @@ def __init__(self, cfg: SimulationCfg | None = None): # store input if cfg is None: cfg = SimulationCfg() + # check that the config is valid + cfg.validate() self.cfg = cfg # check that simulation is running if stage_utils.get_current_stage() is None: diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_generator_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_generator_cfg.py index 5b692bfd81..4b1e9a077a 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_generator_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_generator_cfg.py @@ -88,8 +88,12 @@ class SubTerrainBaseCfg: is 0.7. """ - size: tuple[float, float] = MISSING - """The width (along x) and length (along y) of the terrain (in m).""" + size: tuple[float, float] = (10.0, 10.0) + """The width (along x) and length (along y) of the terrain (in m). Defaults to (10.0, 10.0). + + In case the :class:`~omni.isaac.lab.terrains.TerrainImporterCfg` is used, this parameter gets overridden by + :attr:`omni.isaac.lab.scene.TerrainImporterCfg.size` attribute. + """ flat_patch_sampling: dict[str, FlatPatchSamplingCfg] | None = None """Dictionary of configurations for sampling flat patches on the sub-terrain. Defaults to None, diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_importer.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_importer.py index 2dbdcbf9e9..e8834ceb52 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_importer.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_importer.py @@ -67,6 +67,8 @@ def __init__(self, cfg: TerrainImporterCfg): ValueError: If terrain type is 'usd' and no configuration provided for ``usd_path``. ValueError: If terrain type is 'usd' or 'plane' and no configuration provided for ``env_spacing``. """ + # check that the config is valid + cfg.validate() # store inputs self.cfg = cfg self.device = sim_utils.SimulationContext.instance().device # type: ignore diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_importer_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_importer_cfg.py index d6aca9419e..c420ed2844 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_importer_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_importer_cfg.py @@ -36,8 +36,12 @@ class TerrainImporterCfg: All sub-terrains are imported relative to this prim path. """ - num_envs: int = MISSING - """The number of environment origins to consider.""" + num_envs: int = 1 + """The number of environment origins to consider. Defaults to 1. + + In case, the :class:`~omni.isaac.lab.scene.InteractiveSceneCfg` is used, this parameter gets overridden by + :attr:`omni.isaac.lab.scene.InteractiveSceneCfg.num_envs` attribute. + """ terrain_type: Literal["generator", "plane", "usd"] = "generator" """The type of terrain to generate. Defaults to "generator". diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/configclass.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/configclass.py index 7c1e1a9291..5bb9c30183 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/configclass.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/configclass.py @@ -14,7 +14,7 @@ from .dict import class_to_dict, update_class_from_dict -_CONFIGCLASS_METHODS = ["to_dict", "from_dict", "replace", "copy"] +_CONFIGCLASS_METHODS = ["to_dict", "from_dict", "replace", "copy", "validate"] """List of class methods added at runtime to dataclass.""" """ @@ -98,6 +98,7 @@ class EnvCfg: setattr(cls, "from_dict", _update_class_from_dict) setattr(cls, "replace", _replace_class_with_kwargs) setattr(cls, "copy", _copy_class) + setattr(cls, "validate", _validate) # wrap around dataclass cls = dataclass(cls, **kwargs) # return wrapped class @@ -240,6 +241,56 @@ class State: cls.__annotations__ = hints +def _validate(obj: object, prefix: str = "") -> list[str]: + """Check the validity of configclass object. + + This function checks if the object is a valid configclass object. A valid configclass object contains no MISSING + entries. + + Args: + obj: The object to check. + prefix: The prefix to add to the missing fields. Defaults to ''. + + Returns: + A list of missing fields. + + Raises: + TypeError: When the object is not a valid configuration object. + """ + missing_fields = [] + + if type(obj) is type(MISSING): + missing_fields.append(prefix) + return missing_fields + elif isinstance(obj, (list, tuple)): + for index, item in enumerate(obj): + current_path = f"{prefix}[{index}]" + missing_fields.extend(_validate(item, prefix=current_path)) + return missing_fields + elif isinstance(obj, dict): + obj_dict = obj + elif hasattr(obj, "__dict__"): + obj_dict = obj.__dict__ + else: + return missing_fields + + for key, value in obj_dict.items(): + # disregard builtin attributes + if key.startswith("__"): + continue + current_path = f"{prefix}.{key}" if prefix else key + missing_fields.extend(_validate(value, prefix=current_path)) + + # raise an error only once at the top-level call + if prefix == "" and missing_fields: + formatted_message = "\n".join(f" - {field}" for field in missing_fields) + raise TypeError( + f"Missing values detected in object {obj.__class__.__name__} for the following" + f" fields:\n{formatted_message}\n" + ) + return missing_fields + + def _process_mutable_types(cls): """Initialize all mutable elements through :obj:`dataclasses.Field` to avoid unnecessary complaints. diff --git a/source/extensions/omni.isaac.lab/test/envs/test_direct_marl_env.py b/source/extensions/omni.isaac.lab/test/envs/test_direct_marl_env.py index 01afb1c22c..eba2895e30 100644 --- a/source/extensions/omni.isaac.lab/test/envs/test_direct_marl_env.py +++ b/source/extensions/omni.isaac.lab/test/envs/test_direct_marl_env.py @@ -52,6 +52,7 @@ class EmptyEnvCfg(DirectMARLEnvCfg): action_spaces = {"agent_0": 1, "agent_1": 2} observation_spaces = {"agent_0": 3, "agent_1": 4} state_space = -1 + episode_length_s = 100.0 return EmptyEnvCfg() @@ -72,10 +73,10 @@ def test_initialization(self): # create environment env = DirectMARLEnv(cfg=get_empty_base_env_cfg(device=device)) except Exception as e: - if "env" in locals(): + if "env" in locals() and hasattr(env, "_is_closed"): env.close() else: - if hasattr(e, "obj") and hasattr(e.obj, "close"): + if hasattr(e, "obj") and hasattr(e.obj, "_is_closed"): e.obj.close() self.fail(f"Failed to set-up the DirectMARLEnv environment. Error: {e}") diff --git a/source/extensions/omni.isaac.lab/test/envs/test_env_rendering_logic.py b/source/extensions/omni.isaac.lab/test/envs/test_env_rendering_logic.py index dc6ba8c16f..378a089750 100644 --- a/source/extensions/omni.isaac.lab/test/envs/test_env_rendering_logic.py +++ b/source/extensions/omni.isaac.lab/test/envs/test_env_rendering_logic.py @@ -47,6 +47,7 @@ class EnvCfg(ManagerBasedEnvCfg): """Configuration for the test environment.""" decimation: int = 4 + episode_length_s: float = 100.0 sim: SimulationCfg = SimulationCfg(dt=0.005, render_interval=render_interval) scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=1, env_spacing=1.0) actions: EmptyManagerCfg = EmptyManagerCfg() @@ -63,10 +64,13 @@ class EnvCfg(ManagerBasedRLEnvCfg): """Configuration for the test environment.""" decimation: int = 4 + episode_length_s: float = 100.0 sim: SimulationCfg = SimulationCfg(dt=0.005, render_interval=render_interval) scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=1, env_spacing=1.0) actions: EmptyManagerCfg = EmptyManagerCfg() observations: EmptyManagerCfg = EmptyManagerCfg() + rewards: EmptyManagerCfg = EmptyManagerCfg() + terminations: EmptyManagerCfg = EmptyManagerCfg() return ManagerBasedRLEnv(cfg=EnvCfg()) @@ -81,6 +85,7 @@ class EnvCfg(DirectRLEnvCfg): decimation: int = 4 action_space: int = 0 observation_space: int = 0 + episode_length_s: float = 100.0 sim: SimulationCfg = SimulationCfg(dt=0.005, render_interval=render_interval) scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=1, env_spacing=1.0) @@ -140,10 +145,10 @@ def test_env_rendering_logic(self): else: env = create_direct_rl_env(render_interval) except Exception as e: - if "env" in locals(): + if "env" in locals() and hasattr(env, "_is_closed"): env.close() else: - if hasattr(e, "obj") and hasattr(e.obj, "close"): + if hasattr(e, "obj") and hasattr(e.obj, "_is_closed"): e.obj.close() self.fail(f"Failed to set-up the environment {env_type}. Error: {e}") diff --git a/source/extensions/omni.isaac.lab/test/managers/test_event_manager.py b/source/extensions/omni.isaac.lab/test/managers/test_event_manager.py index 89f43e7060..5a92b7c28d 100644 --- a/source/extensions/omni.isaac.lab/test/managers/test_event_manager.py +++ b/source/extensions/omni.isaac.lab/test/managers/test_event_manager.py @@ -134,6 +134,15 @@ def test_active_terms(self): self.assertEqual(len(self.event_man.active_terms["reset"]), 1) self.assertEqual(len(self.event_man.active_terms["custom"]), 2) + def test_config_empty(self): + """Test the creation of reward manager with empty config.""" + self.event_man = EventManager(None, self.env) + self.assertEqual(len(self.event_man.active_terms), 0) + + # print the expected string + print() + print(self.event_man) + def test_invalid_event_func_module(self): """Test the handling of invalid event function's module in string representation.""" cfg = { diff --git a/source/extensions/omni.isaac.lab/test/managers/test_reward_manager.py b/source/extensions/omni.isaac.lab/test/managers/test_reward_manager.py index 381886776f..af5d35d858 100644 --- a/source/extensions/omni.isaac.lab/test/managers/test_reward_manager.py +++ b/source/extensions/omni.isaac.lab/test/managers/test_reward_manager.py @@ -12,6 +12,7 @@ """Rest everything follows.""" +import torch import unittest from collections import namedtuple @@ -123,6 +124,21 @@ def test_compute(self): self.assertEqual(float(rewards[0]), expected_reward) self.assertEqual(tuple(rewards.shape), (self.env.num_envs,)) + def test_config_empty(self): + """Test the creation of reward manager with empty config.""" + self.rew_man = RewardManager(None, self.env) + self.assertEqual(len(self.rew_man.active_terms), 0) + + # print the expected string + print() + print(self.rew_man) + + # compute reward + rewards = self.rew_man.compute(dt=self.env.dt) + + # check all rewards are zero + torch.testing.assert_close(rewards, torch.zeros_like(rewards)) + def test_active_terms(self): """Test the correct reading of active terms.""" cfg = { diff --git a/source/extensions/omni.isaac.lab/test/scene/test_interactive_scene.py b/source/extensions/omni.isaac.lab/test/scene/test_interactive_scene.py index 4e9d08e54b..70d149fd1e 100644 --- a/source/extensions/omni.isaac.lab/test/scene/test_interactive_scene.py +++ b/source/extensions/omni.isaac.lab/test/scene/test_interactive_scene.py @@ -40,7 +40,7 @@ class MySceneCfg(InteractiveSceneCfg): prim_path="/World/Robot", spawn=sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/Simple/revolute_articulation.usd"), actuators={ - "joint": ImplicitActuatorCfg(), + "joint": ImplicitActuatorCfg(joint_names_expr=[".*"], stiffness=100.0, damping=1.0), }, ) # rigid object diff --git a/source/extensions/omni.isaac.lab/test/utils/test_configclass.py b/source/extensions/omni.isaac.lab/test/utils/test_configclass.py index 1ee984ce52..f899534033 100644 --- a/source/extensions/omni.isaac.lab/test/utils/test_configclass.py +++ b/source/extensions/omni.isaac.lab/test/utils/test_configclass.py @@ -329,6 +329,45 @@ class NestedDictAndListCfg: list_1: list[EnvCfg] = [EnvCfg(), EnvCfg()] +""" +Dummy configuration: Missing attributes +""" + + +@configclass +class MissingParentDemoCfg: + """Dummy parent configuration with missing fields.""" + + a: int = MISSING + + @configclass + class InsideClassCfg: + """Inner dummy configuration.""" + + @configclass + class InsideInsideClassCfg: + """Inner inner dummy configuration.""" + + a: str = MISSING + + inside: str = MISSING + inside_dict = {"a": MISSING} + inside_nested_dict = {"a": {"b": "hello", "c": MISSING, "d": InsideInsideClassCfg()}} + inside_tuple = (10, MISSING, 20) + inside_list = [MISSING, MISSING, 2] + + b: InsideClassCfg = InsideClassCfg() + + +@configclass +class MissingChildDemoCfg(MissingParentDemoCfg): + """Dummy child configuration with missing fields.""" + + c: Callable = MISSING + d: int | None = None + e: dict = {} + + """ Test solutions: Basic """ @@ -404,6 +443,22 @@ class NestedDictAndListCfg: "func_in_dict": {"func": "__main__:dummy_function2"}, } +""" +Test solutions: Missing attributes +""" + +validity_expected_fields = [ + "a", + "b.inside", + "b.inside_dict.a", + "b.inside_nested_dict.a.c", + "b.inside_nested_dict.a.d.a", + "b.inside_tuple[1]", + "b.inside_list[0]", + "b.inside_list[1]", + "c", +] + """ Test fixtures. """ @@ -888,6 +943,22 @@ def test_config_md5_hash(self): self.assertEqual(md5_hash_1, md5_hash_2) + def test_validity(self): + """Check that invalid configurations raise errors.""" + + cfg = MissingChildDemoCfg() + + with self.assertRaises(TypeError) as context: + cfg.validate() + + # check that the expected missing fields are in the error message + error_message = str(context.exception) + for elem in validity_expected_fields: + self.assertIn(elem, error_message) + + # check that no more than the expected missing fields are in the error message + self.assertEqual(len(error_message.split("\n")) - 2, len(validity_expected_fields)) + if __name__ == "__main__": run_tests() diff --git a/source/extensions/omni.isaac.lab_tasks/config/extension.toml b/source/extensions/omni.isaac.lab_tasks/config/extension.toml index 002927c1b2..a739dc74a0 100644 --- a/source/extensions/omni.isaac.lab_tasks/config/extension.toml +++ b/source/extensions/omni.isaac.lab_tasks/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.10.8" +version = "0.10.9" # Description title = "Isaac Lab Environments" diff --git a/source/extensions/omni.isaac.lab_tasks/docs/CHANGELOG.rst b/source/extensions/omni.isaac.lab_tasks/docs/CHANGELOG.rst index 0f55936604..5bf5d9eeea 100644 --- a/source/extensions/omni.isaac.lab_tasks/docs/CHANGELOG.rst +++ b/source/extensions/omni.isaac.lab_tasks/docs/CHANGELOG.rst @@ -1,6 +1,16 @@ Changelog --------- +0.10.9 (2024-10-22) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Sets curriculum and commands to None in manager-based environment configurations when not needed. + Earlier, this was done by making an empty configuration object, which is now unnecessary. + + 0.10.8 (2024-10-22) ~~~~~~~~~~~~~~~~~~~ diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/ant/ant_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/ant/ant_env_cfg.py index f12a046305..68f091c515 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/ant/ant_env_cfg.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/ant/ant_env_cfg.py @@ -58,14 +58,6 @@ class MySceneCfg(InteractiveSceneCfg): ## -@configclass -class CommandsCfg: - """Command terms for the MDP.""" - - # no commands for this MDP - null = mdp.NullCommandCfg() - - @configclass class ActionsCfg: """Action specifications for the MDP.""" @@ -163,13 +155,6 @@ class TerminationsCfg: torso_height = DoneTerm(func=mdp.root_height_below_minimum, params={"minimum_height": 0.31}) -@configclass -class CurriculumCfg: - """Curriculum terms for the MDP.""" - - pass - - @configclass class AntEnvCfg(ManagerBasedRLEnvCfg): """Configuration for the MuJoCo-style Ant walking environment.""" @@ -179,13 +164,10 @@ class AntEnvCfg(ManagerBasedRLEnvCfg): # Basic settings observations: ObservationsCfg = ObservationsCfg() actions: ActionsCfg = ActionsCfg() - commands: CommandsCfg = CommandsCfg() - # MDP settings rewards: RewardsCfg = RewardsCfg() terminations: TerminationsCfg = TerminationsCfg() events: EventCfg = EventCfg() - curriculum: CurriculumCfg = CurriculumCfg() def __post_init__(self): """Post initialization.""" diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py index 8c92d3d5ae..84d88cba10 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py @@ -60,14 +60,6 @@ class CartpoleSceneCfg(InteractiveSceneCfg): ## -@configclass -class CommandsCfg: - """Command terms for the MDP.""" - - # no commands for this MDP - null = mdp.NullCommandCfg() - - @configclass class ActionsCfg: """Action specifications for the MDP.""" @@ -162,13 +154,6 @@ class TerminationsCfg: ) -@configclass -class CurriculumCfg: - """Configuration for the curriculum.""" - - pass - - ## # Environment configuration ## @@ -185,11 +170,8 @@ class CartpoleEnvCfg(ManagerBasedRLEnvCfg): actions: ActionsCfg = ActionsCfg() events: EventCfg = EventCfg() # MDP settings - curriculum: CurriculumCfg = CurriculumCfg() rewards: RewardsCfg = RewardsCfg() terminations: TerminationsCfg = TerminationsCfg() - # No command generator - commands: CommandsCfg = CommandsCfg() # Post initialization def __post_init__(self) -> None: diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/humanoid/humanoid_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/humanoid/humanoid_env_cfg.py index f376811a43..e02dd94edb 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/humanoid/humanoid_env_cfg.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/humanoid/humanoid_env_cfg.py @@ -102,14 +102,6 @@ class MySceneCfg(InteractiveSceneCfg): ## -@configclass -class CommandsCfg: - """Command terms for the MDP.""" - - # no commands for this MDP - null = mdp.NullCommandCfg() - - @configclass class ActionsCfg: """Action specifications for the MDP.""" @@ -248,13 +240,6 @@ class TerminationsCfg: torso_height = DoneTerm(func=mdp.root_height_below_minimum, params={"minimum_height": 0.8}) -@configclass -class CurriculumCfg: - """Curriculum terms for the MDP.""" - - pass - - @configclass class HumanoidEnvCfg(ManagerBasedRLEnvCfg): """Configuration for the MuJoCo-style Humanoid walking environment.""" @@ -264,13 +249,10 @@ class HumanoidEnvCfg(ManagerBasedRLEnvCfg): # Basic settings observations: ObservationsCfg = ObservationsCfg() actions: ActionsCfg = ActionsCfg() - commands: CommandsCfg = CommandsCfg() - # MDP settings rewards: RewardsCfg = RewardsCfg() terminations: TerminationsCfg = TerminationsCfg() events: EventCfg = EventCfg() - curriculum: CurriculumCfg = CurriculumCfg() def __post_init__(self): """Post initialization.""" diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py index 986b10da33..fba2e69ee1 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py @@ -293,13 +293,6 @@ class SpotTerminationsCfg: ) -@configclass -class SpotCurriculumCfg: - """Curriculum terms for the MDP.""" - - pass - - @configclass class SpotFlatEnvCfg(LocomotionVelocityRoughEnvCfg): @@ -312,7 +305,6 @@ class SpotFlatEnvCfg(LocomotionVelocityRoughEnvCfg): rewards: SpotRewardsCfg = SpotRewardsCfg() terminations: SpotTerminationsCfg = SpotTerminationsCfg() events: SpotEventCfg = SpotEventCfg() - curriculum: SpotCurriculumCfg = SpotCurriculumCfg() # Viewer viewer = ViewerCfg(eye=(10.5, 10.5, 0.3), origin_type="world", env_index=0, asset_name="robot") diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/cabinet/cabinet_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/cabinet/cabinet_env_cfg.py index 7faa94601a..56c7e5d2b8 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/cabinet/cabinet_env_cfg.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/cabinet/cabinet_env_cfg.py @@ -123,13 +123,6 @@ class CabinetSceneCfg(InteractiveSceneCfg): ## -@configclass -class CommandsCfg: - """Command terms for the MDP.""" - - null_command = mdp.NullCommandCfg() - - @configclass class ActionsCfg: """Action specifications for the MDP.""" @@ -267,7 +260,6 @@ class CabinetEnvCfg(ManagerBasedRLEnvCfg): # Basic settings observations: ObservationsCfg = ObservationsCfg() actions: ActionsCfg = ActionsCfg() - commands: CommandsCfg = CommandsCfg() # MDP settings rewards: RewardsCfg = RewardsCfg() terminations: TerminationsCfg = TerminationsCfg() diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/navigation/config/anymal_c/navigation_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/navigation/config/anymal_c/navigation_env_cfg.py index 6f5f00a025..c13875c545 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/navigation/config/anymal_c/navigation_env_cfg.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/navigation/config/anymal_c/navigation_env_cfg.py @@ -107,13 +107,6 @@ class CommandsCfg: ) -@configclass -class CurriculumCfg: - """Curriculum terms for the MDP.""" - - pass - - @configclass class TerminationsCfg: """Termination terms for the MDP.""" @@ -127,14 +120,16 @@ class TerminationsCfg: @configclass class NavigationEnvCfg(ManagerBasedRLEnvCfg): + """Configuration for the navigation environment.""" + + # environment settings scene: SceneEntityCfg = LOW_LEVEL_ENV_CFG.scene - commands: CommandsCfg = CommandsCfg() actions: ActionsCfg = ActionsCfg() observations: ObservationsCfg = ObservationsCfg() - rewards: RewardsCfg = RewardsCfg() events: EventCfg = EventCfg() - - curriculum: CurriculumCfg = CurriculumCfg() + # mdp settings + commands: CommandsCfg = CommandsCfg() + rewards: RewardsCfg = RewardsCfg() terminations: TerminationsCfg = TerminationsCfg() def __post_init__(self): diff --git a/source/extensions/omni.isaac.lab_tasks/test/test_environment_determinism.py b/source/extensions/omni.isaac.lab_tasks/test/test_environment_determinism.py index 3346e8284d..3aa2977935 100644 --- a/source/extensions/omni.isaac.lab_tasks/test/test_environment_determinism.py +++ b/source/extensions/omni.isaac.lab_tasks/test/test_environment_determinism.py @@ -101,13 +101,20 @@ def _obtain_transition_tuples( """Run random actions and obtain transition tuples after fixed number of steps.""" # create a new stage omni.usd.get_context().new_stage() - # parse configuration - env_cfg = parse_env_cfg(task_name, device=device, num_envs=num_envs) - # set seed - env_cfg.seed = 42 - - # create environment - env = gym.make(task_name, cfg=env_cfg) + try: + # parse configuration + env_cfg = parse_env_cfg(task_name, device=device, num_envs=num_envs) + # set seed + env_cfg.seed = 42 + # create environment + env = gym.make(task_name, cfg=env_cfg) + except Exception as e: + if "env" in locals() and hasattr(env, "_is_closed"): + env.close() + else: + if hasattr(e, "obj") and hasattr(e.obj, "_is_closed"): + e.obj.close() + self.fail(f"Failed to set-up the environment for task {task_name}. Error: {e}") # disable control on stop env.unwrapped.sim._app_control_on_stop_handle = None # type: ignore diff --git a/source/extensions/omni.isaac.lab_tasks/test/test_environments.py b/source/extensions/omni.isaac.lab_tasks/test/test_environments.py index cfb540e1af..440b35d7c8 100644 --- a/source/extensions/omni.isaac.lab_tasks/test/test_environments.py +++ b/source/extensions/omni.isaac.lab_tasks/test/test_environments.py @@ -100,10 +100,10 @@ def _check_random_actions(self, task_name: str, device: str, num_envs: int, num_ # create environment env = gym.make(task_name, cfg=env_cfg) except Exception as e: - if "env" in locals(): + if "env" in locals() and hasattr(env, "_is_closed"): env.close() else: - if hasattr(e, "obj") and hasattr(e.obj, "close"): + if hasattr(e, "obj") and hasattr(e.obj, "_is_closed"): e.obj.close() self.fail(f"Failed to set-up the environment for task {task_name}. Error: {e}") diff --git a/source/extensions/omni.isaac.lab_tasks/test/test_multi_agent_environments.py b/source/extensions/omni.isaac.lab_tasks/test/test_multi_agent_environments.py index 11a20ef9a3..d65a39bc45 100644 --- a/source/extensions/omni.isaac.lab_tasks/test/test_multi_agent_environments.py +++ b/source/extensions/omni.isaac.lab_tasks/test/test_multi_agent_environments.py @@ -39,7 +39,6 @@ def setUpClass(cls): cls.registered_tasks.append(task_spec.id) # sort environments by name cls.registered_tasks.sort() - cls.registered_tasks = ["Isaac-Shadow-Hand-Over-Direct-v0"] # print all existing task names print(">>> All registered environments:", cls.registered_tasks) @@ -97,10 +96,10 @@ def _check_random_actions(self, task_name: str, device: str, num_envs: int, num_ # create environment env: DirectMARLEnv = gym.make(task_name, cfg=env_cfg) except Exception as e: - if "env" in locals(): + if "env" in locals() and hasattr(env, "_is_closed"): env.close() else: - if hasattr(e, "obj") and hasattr(e.obj, "close"): + if hasattr(e, "obj") and hasattr(e.obj, "_is_closed"): e.obj.close() self.fail(f"Failed to set-up the environment for task {task_name}. Error: {e}") diff --git a/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_rl_games_wrapper.py b/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_rl_games_wrapper.py index 3c05b797db..0cbf01ea75 100644 --- a/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_rl_games_wrapper.py +++ b/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_rl_games_wrapper.py @@ -69,10 +69,10 @@ def test_random_actions(self): # wrap environment env = RlGamesVecEnvWrapper(env, "cuda:0", 100, 100) except Exception as e: - if "env" in locals(): + if "env" in locals() and hasattr(env, "_is_closed"): env.close() else: - if hasattr(e, "obj") and hasattr(e.obj, "close"): + if hasattr(e, "obj") and hasattr(e.obj, "_is_closed"): e.obj.close() self.fail(f"Failed to set-up the environment for task {task_name}. Error: {e}") diff --git a/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_rsl_rl_wrapper.py b/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_rsl_rl_wrapper.py index 271747d2ac..c4e7c797f2 100644 --- a/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_rsl_rl_wrapper.py +++ b/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_rsl_rl_wrapper.py @@ -69,10 +69,10 @@ def test_random_actions(self): # wrap environment env = RslRlVecEnvWrapper(env) except Exception as e: - if "env" in locals(): + if "env" in locals() and hasattr(env, "_is_closed"): env.close() else: - if hasattr(e, "obj") and hasattr(e.obj, "close"): + if hasattr(e, "obj") and hasattr(e.obj, "_is_closed"): e.obj.close() self.fail(f"Failed to set-up the environment for task {task_name}. Error: {e}") diff --git a/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_sb3_wrapper.py b/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_sb3_wrapper.py index f6d9d1abb4..d0a9aa507c 100644 --- a/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_sb3_wrapper.py +++ b/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_sb3_wrapper.py @@ -70,10 +70,10 @@ def test_random_actions(self): # wrap environment env = Sb3VecEnvWrapper(env) except Exception as e: - if "env" in locals(): + if "env" in locals() and hasattr(env, "_is_closed"): env.close() else: - if hasattr(e, "obj") and hasattr(e.obj, "close"): + if hasattr(e, "obj") and hasattr(e.obj, "_is_closed"): e.obj.close() self.fail(f"Failed to set-up the environment for task {task_name}. Error: {e}") diff --git a/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_skrl_wrapper.py b/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_skrl_wrapper.py index 2c925d34b4..58085c1671 100644 --- a/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_skrl_wrapper.py +++ b/source/extensions/omni.isaac.lab_tasks/test/wrappers/test_skrl_wrapper.py @@ -68,10 +68,10 @@ def test_random_actions(self): # wrap environment env = SkrlVecEnvWrapper(env) except Exception as e: - if "env" in locals(): + if "env" in locals() and hasattr(env, "_is_closed"): env.close() else: - if hasattr(e, "obj") and hasattr(e.obj, "close"): + if hasattr(e, "obj") and hasattr(e.obj, "_is_closed"): e.obj.close() self.fail(f"Failed to set-up the environment for task {task_name}. Error: {e}") # reset environment