diff --git a/.github/workflows/mirror-buildspec.yml b/.github/workflows/mirror-buildspec.yml new file mode 100644 index 0000000000..a537f5bfd9 --- /dev/null +++ b/.github/workflows/mirror-buildspec.yml @@ -0,0 +1,15 @@ +version: 0.2 + +phases: + install: + runtime-versions: + nodejs: 14 + pre_build: + commands: + - git config --global user.name "Isaac LAB CI Bot" + - git config --global user.email "isaac-lab-ci-bot@nvidia.com" + build: + commands: + - git remote set-url origin https://github.com/${TARGET_REPO}.git + - git checkout $SOURCE_BRANCH + - git push --force https://$GITHUB_TOKEN@github.com/${TARGET_REPO}.git $SOURCE_BRANCH:$TARGET_BRANCH diff --git a/.github/workflows/postmerge-ci-buildspec.yml b/.github/workflows/postmerge-ci-buildspec.yml index 94201516bf..e071d7db09 100644 --- a/.github/workflows/postmerge-ci-buildspec.yml +++ b/.github/workflows/postmerge-ci-buildspec.yml @@ -3,10 +3,42 @@ version: 0.2 phases: build: commands: - - echo "Building a docker image" - - docker login -u \$oauthtoken -p $NGC_TOKEN nvcr.io - - docker build -t $IMAGE_NAME:latest-1.2 --build-arg ISAACSIM_VERSION_ARG=4.2.0 --build-arg ISAACSIM_ROOT_PATH_ARG=/isaac-sim --build-arg ISAACLAB_PATH_ARG=/workspace/isaaclab --build-arg DOCKER_USER_HOME_ARG=/root -f docker/Dockerfile.base . - - echo "Pushing the docker image" - - docker push $IMAGE_NAME:latest-1.2 - - docker tag $IMAGE_NAME:latest-1.2 $IMAGE_NAME:latest-1.2-b$CODEBUILD_BUILD_NUMBER - - docker push $IMAGE_NAME:latest-1.2-b$CODEBUILD_BUILD_NUMBER + - echo "Building and pushing Docker image" + - | + # Determine branch name or use fallback + if [ -n "$CODEBUILD_WEBHOOK_HEAD_REF" ]; then + BRANCH_NAME=$(echo $CODEBUILD_WEBHOOK_HEAD_REF | sed 's/refs\/heads\///') + elif [ -n "$CODEBUILD_SOURCE_VERSION" ]; then + BRANCH_NAME=$CODEBUILD_SOURCE_VERSION + else + BRANCH_NAME="unknown" + fi + + # Replace '/' with '-' and remove any invalid characters for Docker tag + SAFE_BRANCH_NAME=$(echo $BRANCH_NAME | sed 's/[^a-zA-Z0-9._-]/-/g') + + # Use "latest" if branch name is empty or only contains invalid characters + if [ -z "$SAFE_BRANCH_NAME" ]; then + SAFE_BRANCH_NAME="latest" + fi + + # Get the git repository short name + REPO_SHORT_NAME=$(basename -s .git `git config --get remote.origin.url`) + if [ -z "$REPO_SHORT_NAME" ]; then + REPO_SHORT_NAME="verification" + fi + + # Combine repo short name and branch name for the tag + COMBINED_TAG="${REPO_SHORT_NAME}-${SAFE_BRANCH_NAME}" + + docker login -u \$oauthtoken -p $NGC_TOKEN nvcr.io + docker build -t $IMAGE_NAME:$COMBINED_TAG \ + --build-arg ISAACSIM_BASE_IMAGE_ARG=$ISAACSIM_BASE_IMAGE \ + --build-arg ISAACSIM_VERSION_ARG=4.2.0 \ + --build-arg ISAACSIM_ROOT_PATH_ARG=/isaac-sim \ + --build-arg ISAACLAB_PATH_ARG=/workspace/isaaclab \ + --build-arg DOCKER_USER_HOME_ARG=/root \ + -f docker/Dockerfile.base . + docker push $IMAGE_NAME:$COMBINED_TAG + docker tag $IMAGE_NAME:$COMBINED_TAG $IMAGE_NAME:$COMBINED_TAG-b$CODEBUILD_BUILD_NUMBER + docker push $IMAGE_NAME:$COMBINED_TAG-b$CODEBUILD_BUILD_NUMBER diff --git a/.github/workflows/premerge-ci-buildspec.yml b/.github/workflows/premerge-ci-buildspec.yml index bed8a5cf2e..4fa2372b4e 100644 --- a/.github/workflows/premerge-ci-buildspec.yml +++ b/.github/workflows/premerge-ci-buildspec.yml @@ -4,15 +4,39 @@ phases: pre_build: commands: - echo "Launching EC2 instance to run tests" - - INSTANCE_ID=$(aws ec2 run-instances --image-id ami-0f7f7fb14ee15d5ae --count 1 --instance-type g5.2xlarge --key-name production/ssh/isaaclab --security-group-ids sg-02617e4b8916794c4 --subnet-id subnet-0907ceaeb40fd9eac --block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=500}' --output text --query 'Instances[0].InstanceId') + - | + INSTANCE_ID=$(aws ec2 run-instances \ + --image-id ami-0b3a9d48380433e49 \ + --count 1 \ + --instance-type g5.2xlarge \ + --key-name production/ssh/isaaclab \ + --security-group-ids sg-02617e4b8916794c4 \ + --subnet-id subnet-0907ceaeb40fd9eac \ + --block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":500}}]' \ + --output text \ + --query 'Instances[0].InstanceId') - aws ec2 wait instance-running --instance-ids $INSTANCE_ID - - EC2_INSTANCE_IP=$(aws ec2 describe-instances --filters "Name=instance-state-name,Values=running" "Name=instance-id,Values=$INSTANCE_ID" --query 'Reservations[*].Instances[*].[PrivateIpAddress]' --output text) + - | + EC2_INSTANCE_IP=$(aws ec2 describe-instances \ + --filters "Name=instance-state-name,Values=running" "Name=instance-id,Values=$INSTANCE_ID" \ + --query 'Reservations[*].Instances[*].[PrivateIpAddress]' \ + --output text) - mkdir -p ~/.ssh - - aws ec2 describe-key-pairs --include-public-key --key-name production/ssh/isaaclab --query 'KeyPairs[0].PublicKey' --output text > ~/.ssh/id_rsa.pub - - aws secretsmanager get-secret-value --secret-id production/ssh/isaaclab --query SecretString --output text > ~/.ssh/id_rsa + - | + aws ec2 describe-key-pairs --include-public-key --key-name production/ssh/isaaclab \ + --query 'KeyPairs[0].PublicKey' --output text > ~/.ssh/id_rsa.pub + - | + aws secretsmanager get-secret-value --secret-id production/ssh/isaaclab \ + --query SecretString --output text > ~/.ssh/id_rsa - chmod 400 ~/.ssh/id_* - echo "Host $EC2_INSTANCE_IP\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config - - aws ec2-instance-connect send-ssh-public-key --instance-id $INSTANCE_ID --availability-zone us-west-2a --ssh-public-key file://~/.ssh/id_rsa.pub --instance-os-user ubuntu + - | + aws ec2-instance-connect send-ssh-public-key \ + --instance-id $INSTANCE_ID \ + --availability-zone us-west-2a \ + --ssh-public-key file://~/.ssh/id_rsa.pub \ + --instance-os-user ubuntu + build: commands: - echo "Running tests on EC2 instance" @@ -40,10 +64,20 @@ phases: retry_scp ' - ssh ubuntu@$EC2_INSTANCE_IP "docker login -u \\\$oauthtoken -p $NGC_TOKEN nvcr.io" - - ssh ubuntu@$EC2_INSTANCE_IP "cd $SRC_DIR; - DOCKER_BUILDKIT=1 docker build -t isaac-lab-dev --build-arg ISAACSIM_VERSION_ARG=4.2.0 --build-arg ISAACSIM_ROOT_PATH_ARG=/isaac-sim --build-arg ISAACLAB_PATH_ARG=/workspace/isaaclab --build-arg DOCKER_USER_HOME_ARG=/root -f docker/Dockerfile.base . ; - docker run --rm --entrypoint bash --gpus all --network=host --name isaac-lab-test isaac-lab-dev ./isaaclab.sh -t && - exit $?" + - | + ssh ubuntu@$EC2_INSTANCE_IP " + cd $SRC_DIR + DOCKER_BUILDKIT=1 docker build -t isaac-lab-dev \ + --build-arg ISAACSIM_BASE_IMAGE_ARG=$ISAACSIM_BASE_IMAGE \ + --build-arg ISAACSIM_VERSION_ARG=4.2.0 \ + --build-arg ISAACSIM_ROOT_PATH_ARG=/isaac-sim \ + --build-arg ISAACLAB_PATH_ARG=/workspace/isaaclab \ + --build-arg DOCKER_USER_HOME_ARG=/root \ + -f docker/Dockerfile.base . + docker run --rm --entrypoint bash --gpus all --network=host \ + --name isaac-lab-test isaac-lab-dev ./isaaclab.sh -t && exit \$? + " + post_build: commands: - echo "Terminating EC2 instance" diff --git a/docker/.env.base b/docker/.env.base index cb5de785b7..0ec3332df3 100644 --- a/docker/.env.base +++ b/docker/.env.base @@ -4,6 +4,8 @@ # Accept the NVIDIA Omniverse EULA by default ACCEPT_EULA=Y +# NVIDIA Isaac Sim base image +ISAACSIM_BASE_IMAGE=nvcr.io/nvidia/isaac-sim # NVIDIA Isaac Sim version to use (e.g. 4.2.0) ISAACSIM_VERSION=4.2.0 # Derived from the default path in the NVIDIA provided Isaac Sim container diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base index 8a6c7250b4..7bdb040332 100644 --- a/docker/Dockerfile.base +++ b/docker/Dockerfile.base @@ -7,8 +7,9 @@ # Please check above link for license information. # Base image +ARG ISAACSIM_BASE_IMAGE_ARG ARG ISAACSIM_VERSION_ARG -FROM nvcr.io/nvidia/isaac-sim:${ISAACSIM_VERSION_ARG} AS base +FROM ${ISAACSIM_BASE_IMAGE_ARG}:${ISAACSIM_VERSION_ARG} AS base ENV ISAACSIM_VERSION=${ISAACSIM_VERSION_ARG} # Set default RUN shell to bash diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index baf531bdfd..5e694040b2 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -79,6 +79,7 @@ services: context: ../ dockerfile: docker/Dockerfile.base args: + - ISAACSIM_BASE_IMAGE_ARG=${ISAACSIM_BASE_IMAGE} - ISAACSIM_VERSION_ARG=${ISAACSIM_VERSION} - ISAACSIM_ROOT_PATH_ARG=${DOCKER_ISAACSIM_ROOT_PATH} - ISAACLAB_PATH_ARG=${DOCKER_ISAACLAB_PATH} diff --git a/docs/source/_static/demos/multi_asset.jpg b/docs/source/_static/demos/multi_asset.jpg new file mode 100644 index 0000000000..c7124d20e6 Binary files /dev/null and b/docs/source/_static/demos/multi_asset.jpg differ diff --git a/docs/source/api/lab/omni.isaac.lab.sim.spawners.rst b/docs/source/api/lab/omni.isaac.lab.sim.spawners.rst index 6e3ca9aa9e..a1c073d4c2 100644 --- a/docs/source/api/lab/omni.isaac.lab.sim.spawners.rst +++ b/docs/source/api/lab/omni.isaac.lab.sim.spawners.rst @@ -13,6 +13,7 @@ sensors from_files materials + wrappers .. rubric:: Classes @@ -302,3 +303,27 @@ Physical Materials .. autoclass:: DeformableBodyMaterialCfg :members: :exclude-members: __init__, func + +Wrappers +-------- + +.. automodule:: omni.isaac.lab.sim.spawners.wrappers + + .. rubric:: Classes + + .. autosummary:: + + MultiAssetSpawnerCfg + MultiUsdFileCfg + +.. autofunction:: spawn_multi_asset + +.. autoclass:: MultiAssetSpawnerCfg + :members: + :exclude-members: __init__, func + +.. autofunction:: spawn_multi_usd_file + +.. autoclass:: MultiUsdFileCfg + :members: + :exclude-members: __init__, func diff --git a/docs/source/how-to/index.rst b/docs/source/how-to/index.rst index 893b3a69b3..4b5c426d82 100644 --- a/docs/source/how-to/index.rst +++ b/docs/source/how-to/index.rst @@ -36,6 +36,17 @@ a fixed base robot. This guide goes over the various considerations and steps to make_fixed_prim +Spawning Multiple Assets +------------------------ + +This guide explains how to import and configure different assets in each environment. This is +useful when you want to create diverse environments with different objects. + +.. toctree:: + :maxdepth: 1 + + multi_asset_spawning + Saving Camera Output -------------------- diff --git a/docs/source/how-to/multi_asset_spawning.rst b/docs/source/how-to/multi_asset_spawning.rst new file mode 100644 index 0000000000..9f74e39f6b --- /dev/null +++ b/docs/source/how-to/multi_asset_spawning.rst @@ -0,0 +1,101 @@ +Spawning Multiple Assets +======================== + +.. currentmodule:: omni.isaac.lab + +Typical, spawning configurations (introduced in the :ref:`tutorial-spawn-prims` tutorial) copy the same +asset (or USD primitive) across the different resolved prim paths from the expressions. +For instance, if the user specifies to spawn the asset at "/World/Table\_.*/Object", the same +asset is created at the paths "/World/Table_0/Object", "/World/Table_1/Object" and so on. + +However, at times, it might be desirable to spawn different assets under the prim paths to +ensure a diversity in the simulation. This guide describes how to create different assets under +each prim path using the spawning functionality. + +The sample script ``multi_asset.py`` is used as a reference, located in the +``IsaacLab/source/standalone/demos`` directory. + +.. dropdown:: Code for multi_asset.py + :icon: code + + .. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :emphasize-lines: 101-123, 130-149 + :linenos: + +This script creates multiple environments, where each environment has a rigid object that is either a cone, +a cube, or a sphere, and an articulation that is either the ANYmal-C or ANYmal-D robot. + +.. image:: ../_static/demos/multi_asset.jpg + :width: 100% + :alt: result of multi_asset.py + +Using Multi-Asset Spawning Functions +------------------------------------ + +It is possible to spawn different assets and USDs in each environment using the spawners +:class:`~sim.spawners.wrappers.MultiAssetSpawnerCfg` and :class:`~sim.spawners.wrappers.MultiUsdFileCfg`: + +* We set the spawn configuration in :class:`~assets.RigidObjectCfg` to be + :class:`~sim.spawners.wrappers.MultiAssetSpawnerCfg`: + + .. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :lines: 99-125 + :dedent: + + This function allows you to define a list of different assets that can be spawned as rigid objects. + When :attr:`~sim.spawners.wrappers.MultiAssetSpawnerCfg.random_choice` is set to True, one asset from the list + is randomly selected and spawned at the specified prim path. + +* Similarly, we set the spawn configuration in :class:`~assets.ArticulationCfg` to be + :class:`~sim.spawners.wrappers.MultiUsdFileCfg`: + + .. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :lines: 128-161 + :dedent: + + Similar to before, this configuration allows the selection of different USD files representing articulated assets. + + +Things to Note +-------------- + +Similar asset structuring +~~~~~~~~~~~~~~~~~~~~~~~~~ + +While spawning and handling multiple assets using the same physics interface (the rigid object or articulation classes), +it is essential to have the assets at all the prim locations follow a similar structure. In case of an articulation, +this means that they all must have the same number of links and joints, the same number of collision bodies and +the same names for them. If that is not the case, the physics parsing of the prims can get affected and fail. + +The main purpose of this functionality is to enable the user to create randomized versions of the same asset, +for example robots with different link lengths, or rigid objects with different collider shapes. + +Disabling physics replication in interactive scene +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the flag :attr:`scene.InteractiveScene.replicate_physics` is set to True. This flag informs the physics +engine that the simulation environments are copies of one another so it just needs to parse the first environment +to understand the entire simulation scene. This helps speed up the simulation scene parsing. + +However, in the case of spawning different assets in different environments, this assumption does not hold +anymore. Hence the flag :attr:`scene.InteractiveScene.replicate_physics` must be disabled. + +.. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :lines: 221-224 + :dedent: + +The Code Execution +------------------ + +To execute the script with multiple environments and randomized assets, use the following command: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/demos/multi_asset.py --num_envs 2048 + +This command runs the simulation with 2048 environments, each with randomly selected assets. +To stop the simulation, you can close the window, or press ``Ctrl+C`` in the terminal. diff --git a/docs/source/overview/environments.rst b/docs/source/overview/environments.rst index 274292d5b3..f42f3c34a5 100644 --- a/docs/source/overview/environments.rst +++ b/docs/source/overview/environments.rst @@ -69,8 +69,8 @@ Classic environments that are based on IsaacGymEnvs implementation of MuJoCo-sty .. |humanoid-link| replace:: `Isaac-Humanoid-v0 `__ .. |ant-link| replace:: `Isaac-Ant-v0 `__ .. |cartpole-link| replace:: `Isaac-Cartpole-v0 `__ -.. |cartpole-rgb-link| replace:: `Isaac-Cartpole-RGB-v0 `__ -.. |cartpole-depth-link| replace:: `Isaac-Cartpole-Depth-v0 `__ +.. |cartpole-rgb-link| replace:: `Isaac-Cartpole-RGB-Camera-v0 `__ +.. |cartpole-depth-link| replace:: `Isaac-Cartpole-Depth-Camera-v0 `__ .. |humanoid-direct-link| replace:: `Isaac-Humanoid-Direct-v0 `__ .. |ant-direct-link| replace:: `Isaac-Ant-Direct-v0 `__ diff --git a/docs/source/overview/showroom.rst b/docs/source/overview/showroom.rst index d8ff7a933d..d3d86fd777 100644 --- a/docs/source/overview/showroom.rst +++ b/docs/source/overview/showroom.rst @@ -77,7 +77,7 @@ A few quick showroom scripts to run and checkout: :width: 100% :alt: Dexterous hands in Isaac Lab -- Spawn procedurally generated terrains with different configurations: +- Spawn different deformable (soft) bodies and let them fall from a height: .. tab-set:: :sync-group: os @@ -87,20 +87,20 @@ A few quick showroom scripts to run and checkout: .. code:: bash - ./isaaclab.sh -p source/standalone/demos/procedural_terrain.py + ./isaaclab.sh -p source/standalone/demos/deformables.py .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows .. code:: batch - isaaclab.bat -p source\standalone\demos\procedural_terrain.py + isaaclab.bat -p source\standalone\demos\deformables.py - .. image:: ../_static/demos/procedural_terrain.jpg + .. image:: ../_static/demos/deformables.jpg :width: 100% - :alt: Procedural Terrains in Isaac Lab + :alt: Deformable primitive-shaped objects in Isaac Lab -- Spawn different deformable (soft) bodies and let them fall from a height: +- Use the interactive scene and spawn varying assets in individual environments: .. tab-set:: :sync-group: os @@ -110,20 +110,43 @@ A few quick showroom scripts to run and checkout: .. code:: bash - ./isaaclab.sh -p source/standalone/demos/deformables.py + ./isaaclab.sh -p source/standalone/demos/multi_asset.py .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows .. code:: batch - isaaclab.bat -p source\standalone\demos\deformables.py + isaaclab.bat -p source\standalone\demos\multi_asset.py - .. image:: ../_static/demos/deformables.jpg + .. image:: ../_static/demos/multi_asset.jpg :width: 100% - :alt: Deformable primitive-shaped objects in Isaac Lab + :alt: Multiple assets managed through the same simulation handles + +- Create and spawn procedurally generated terrains with different configurations: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/demos/procedural_terrain.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\demos\procedural_terrain.py + + .. image:: ../_static/demos/procedural_terrain.jpg + :width: 100% + :alt: Procedural Terrains in Isaac Lab -- Spawn multiple markers that are useful for visualizations: +- Define multiple markers that are useful for visualizations: .. tab-set:: :sync-group: os diff --git a/isaaclab.bat b/isaaclab.bat index 53674d71f8..b415ef1a13 100644 --- a/isaaclab.bat +++ b/isaaclab.bat @@ -81,7 +81,7 @@ if errorlevel 1 ( set isaacsim_exe=!isaac_path!\isaac-sim.bat ) else ( rem if isaac sim installed from pip - set isaacsim_exe=isaacsim + set isaacsim_exe=isaacsim omni.isaac.sim ) rem check if there is a python path available if not exist "%isaacsim_exe%" ( diff --git a/isaaclab.sh b/isaaclab.sh index 297424b9c2..a604706e70 100755 --- a/isaaclab.sh +++ b/isaaclab.sh @@ -90,8 +90,14 @@ extract_isaacsim_exe() { local isaacsim_exe=${isaac_path}/isaac-sim.sh # check if there is a python path available if [ ! -f "${isaacsim_exe}" ]; then - echo "[ERROR] No Isaac Sim executable found at path: ${isaacsim_exe}" >&2 - exit 1 + # check for installation using Isaac Sim pip + if [ $(python -m pip list | grep -c 'isaacsim-rl') -gt 0 ]; then + # Isaac Sim - Python packages entry point + local isaacsim_exe="isaacsim omni.isaac.sim" + else + echo "[ERROR] No Isaac Sim executable found at path: ${isaac_path}" >&2 + exit 1 + fi fi # return the result echo ${isaacsim_exe} diff --git a/source/extensions/omni.isaac.lab/config/extension.toml b/source/extensions/omni.isaac.lab/config/extension.toml index 473b7a0986..7e9225c2b3 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.24.19" +version = "0.25.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 2e67c3708c..b0fa66f629 100644 --- a/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst +++ b/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst @@ -1,6 +1,26 @@ Changelog --------- +0.25.0 (2024-10-06) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added configuration classes for spawning assets from a list of individual asset configurations randomly + at the specified prim paths. + + +0.24.20 (2024-10-07) +~~~~~~~~~~~~~~~~~~~~ + +Fixes +^^^^^ + +* Fixed the :meth:`omni.isaac.lab.envs.mdp.events.randomize_rigid_body_material` function to + correctly sample friction and restitution from the given ranges. + + 0.24.19 (2024-10-05) ~~~~~~~~~~~~~~~~~~~~ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/events.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/events.py index c0fb181221..1686797aac 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/events.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/events.py @@ -14,7 +14,6 @@ from __future__ import annotations -import numpy as np import torch from typing import TYPE_CHECKING, Literal @@ -27,7 +26,7 @@ import omni.isaac.lab.utils.math as math_utils from omni.isaac.lab.actuators import ImplicitActuator from omni.isaac.lab.assets import Articulation, DeformableObject, RigidObject -from omni.isaac.lab.managers import SceneEntityCfg +from omni.isaac.lab.managers import EventTermCfg, ManagerTermBase, SceneEntityCfg from omni.isaac.lab.terrains import TerrainImporter if TYPE_CHECKING: @@ -132,15 +131,7 @@ def randomize_scale( scale_spec.default = Gf.Vec3f(*rand_samples[i]) -def randomize_rigid_body_material( - env: ManagerBasedEnv, - env_ids: torch.Tensor | None, - static_friction_range: tuple[float, float], - dynamic_friction_range: tuple[float, float], - restitution_range: tuple[float, float], - num_buckets: int, - asset_cfg: SceneEntityCfg, -): +class randomize_rigid_body_material(ManagerTermBase): """Randomize the physics materials on all geometries of the asset. This function creates a set of physics materials with random static friction, dynamic friction, and restitution @@ -153,6 +144,10 @@ def randomize_rigid_body_material( all bodies). The integer values are used as indices to select the material properties from the material buckets. + If the flag ``make_consistent`` is set to ``True``, the dynamic friction is set to be less than or equal to + the static friction. This obeys the physics constraint on friction values. However, it may not always be + essential for the application. Thus, the flag is set to ``False`` by default. + .. attention:: This function uses CPU tensors to assign the material properties. It is recommended to use this function only during the initialization of the environment. Otherwise, it may lead to a significant performance @@ -160,69 +155,111 @@ def randomize_rigid_body_material( .. note:: PhysX only allows 64000 unique physics materials in the scene. If the number of materials exceeds this - limit, the simulation will crash. + limit, the simulation will crash. Due to this reason, we sample the materials only once during initialization. + Afterwards, these materials are randomly assigned to the geometries of the asset. """ - # extract the used quantities (to enable type-hinting) - asset: RigidObject | Articulation = env.scene[asset_cfg.name] - - if not isinstance(asset, (RigidObject, Articulation)): - raise ValueError( - f"Randomization term 'randomize_rigid_body_material' not supported for asset: '{asset_cfg.name}'" - f" with type: '{type(asset)}'." - ) - # resolve environment ids - if env_ids is None: - env_ids = torch.arange(env.scene.num_envs, device="cpu") - else: - env_ids = env_ids.cpu() + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + """Initialize the term. - # retrieve material buffer - materials = asset.root_physx_view.get_material_properties() + Args: + cfg: The configuration of the event term. + env: The environment instance. - # sample material properties from the given ranges - material_samples = np.zeros(materials[env_ids].shape) - material_samples[..., 0] = np.random.uniform(*static_friction_range) - material_samples[..., 1] = np.random.uniform(*dynamic_friction_range) - material_samples[..., 2] = np.random.uniform(*restitution_range) + Raises: + ValueError: If the asset is not a RigidObject or an Articulation. + """ + super().__init__(cfg, env) - # create uniform range tensor for bucketing - lo = np.array([static_friction_range[0], dynamic_friction_range[0], restitution_range[0]]) - hi = np.array([static_friction_range[1], dynamic_friction_range[1], restitution_range[1]]) + # extract the used quantities (to enable type-hinting) + self.asset_cfg: SceneEntityCfg = cfg.params["asset_cfg"] + self.asset: RigidObject | Articulation = env.scene[self.asset_cfg.name] - # to avoid 64k material limit in physx, we bucket materials by binning randomized material properties - # into buckets based on the number of buckets specified - for d in range(3): - buckets = np.array([(hi[d] - lo[d]) * i / num_buckets + lo[d] for i in range(num_buckets)]) - material_samples[..., d] = buckets[np.searchsorted(buckets, material_samples[..., d]) - 1] + if not isinstance(self.asset, (RigidObject, Articulation)): + raise ValueError( + f"Randomization term 'randomize_rigid_body_material' not supported for asset: '{self.asset_cfg.name}'" + f" with type: '{type(self.asset)}'." + ) - # update material buffer with new samples - if isinstance(asset, Articulation) and asset_cfg.body_ids != slice(None): # obtain number of shapes per body (needed for indexing the material properties correctly) # note: this is a workaround since the Articulation does not provide a direct way to obtain the number of shapes # per body. We use the physics simulation view to obtain the number of shapes per body. - num_shapes_per_body = [] - for link_path in asset.root_physx_view.link_paths[0]: - link_physx_view = asset._physics_sim_view.create_rigid_body_view(link_path) # type: ignore - num_shapes_per_body.append(link_physx_view.max_shapes) + if isinstance(self.asset, Articulation) and self.asset_cfg.body_ids != slice(None): + self.num_shapes_per_body = [] + for link_path in self.asset.root_physx_view.link_paths[0]: + link_physx_view = self.asset._physics_sim_view.create_rigid_body_view(link_path) # type: ignore + self.num_shapes_per_body.append(link_physx_view.max_shapes) + # ensure the parsing is correct + num_shapes = sum(self.num_shapes_per_body) + expected_shapes = self.asset.root_physx_view.max_shapes + if num_shapes != expected_shapes: + raise ValueError( + "Randomization term 'randomize_rigid_body_material' failed to parse the number of shapes per body." + f" Expected total shapes: {expected_shapes}, but got: {num_shapes}." + ) + else: + # in this case, we don't need to do special indexing + self.num_shapes_per_body = None + + # obtain parameters for sampling friction and restitution values + static_friction_range = cfg.params.get("static_friction_range", (1.0, 1.0)) + dynamic_friction_range = cfg.params.get("dynamic_friction_range", (1.0, 1.0)) + restitution_range = cfg.params.get("restitution_range", (0.0, 0.0)) + num_buckets = int(cfg.params.get("num_buckets", 1)) # sample material properties from the given ranges - for body_id in asset_cfg.body_ids: - # start index of shape - start_idx = sum(num_shapes_per_body[:body_id]) - # end index of shape - end_idx = start_idx + num_shapes_per_body[body_id] - # assign the new materials - # material ids are of shape: num_env_ids x num_shapes - # material_buckets are of shape: num_buckets x 3 - materials[env_ids, start_idx:end_idx] = torch.from_numpy(material_samples[:, start_idx:end_idx]).to( - dtype=torch.float - ) - else: - materials[env_ids] = torch.from_numpy(material_samples).to(dtype=torch.float) + # note: we only sample the materials once during initialization + # afterwards these are randomly assigned to the geometries of the asset + range_list = [static_friction_range, dynamic_friction_range, restitution_range] + ranges = torch.tensor(range_list, device="cpu") + self.material_buckets = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (num_buckets, 3), device="cpu") + + # ensure dynamic friction is always less than static friction + make_consistent = cfg.params.get("make_consistent", False) + if make_consistent: + self.material_buckets[:, 1] = torch.min(self.material_buckets[:, 0], self.material_buckets[:, 1]) + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor | None, + static_friction_range: tuple[float, float], + dynamic_friction_range: tuple[float, float], + restitution_range: tuple[float, float], + num_buckets: int, + asset_cfg: SceneEntityCfg, + make_consistent: bool = False, + ): + # resolve environment ids + if env_ids is None: + env_ids = torch.arange(env.scene.num_envs, device="cpu") + else: + env_ids = env_ids.cpu() + + # randomly assign material IDs to the geometries + total_num_shapes = self.asset.root_physx_view.max_shapes + bucket_ids = torch.randint(0, num_buckets, (len(env_ids), total_num_shapes), device="cpu") + material_samples = self.material_buckets[bucket_ids] + + # retrieve material buffer from the physics simulation + materials = self.asset.root_physx_view.get_material_properties() + + # update material buffer with new samples + if self.num_shapes_per_body is not None: + # sample material properties from the given ranges + for body_id in self.asset_cfg.body_ids: + # obtain indices of shapes for the body + start_idx = sum(self.num_shapes_per_body[:body_id]) + end_idx = start_idx + self.num_shapes_per_body[body_id] + # assign the new materials + # material samples are of shape: num_env_ids x total_num_shapes x 3 + materials[env_ids, start_idx:end_idx] = material_samples[:, start_idx:end_idx] + else: + # assign all the materials + materials[env_ids] = material_samples[:] - # apply to simulation - asset.root_physx_view.set_material_properties(materials, env_ids) + # apply to simulation + self.asset.root_physx_view.set_material_properties(materials, env_ids) def randomize_rigid_body_mass( 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 c803f0e305..8c4b81aaff 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 @@ -166,6 +166,18 @@ def clone_environments(self, copy_from_source: bool = False): If True, clones are independent copies of the source prim and won't reflect its changes (start-up time may increase). Defaults to False. """ + # check if user spawned different assets in individual environments + # this flag will be None if no multi asset is spawned + carb_settings_iface = carb.settings.get_settings() + has_multi_assets = carb_settings_iface.get("/isaaclab/spawn/multi_assets") + if has_multi_assets and self.cfg.replicate_physics: + carb.log_warn( + "Varying assets might have been spawned under different environments." + " However, the replicate physics flag is enabled in the 'InteractiveScene' configuration." + " This may adversely affect PhysX parsing. We recommend disabling this property." + ) + + # clone the environment env_origins = self.cloner.clone( source_prim_path=self.env_prim_paths[0], prim_paths=self.env_prim_paths, @@ -187,9 +199,6 @@ def filter_collisions(self, global_prim_paths: list[str] | None = None): global_prim_paths: A list of global prim paths to enable collisions with. Defaults to None, in which case no global prim paths are considered. """ - # obtain the current physics scene - physics_scene_prim_path = self.physics_scene_path - # validate paths in global prim paths if global_prim_paths is None: global_prim_paths = [] @@ -203,7 +212,7 @@ def filter_collisions(self, global_prim_paths: list[str] | None = None): # filter collisions within each environment instance self.cloner.filter_collisions( - physics_scene_prim_path, + self.physics_scene_path, "/World/collisions", self.env_prim_paths, global_paths=self._global_prim_paths, @@ -224,14 +233,16 @@ def __str__(self) -> str: """ @property - def physics_scene_path(self): - """Search the stage for the physics scene""" + def physics_scene_path(self) -> str: + """The path to the USD Physics Scene.""" if self._physics_scene_path is None: for prim in self.stage.Traverse(): if prim.HasAPI(PhysxSchema.PhysxSceneAPI): - self._physics_scene_path = prim.GetPrimPath() + self._physics_scene_path = prim.GetPrimPath().pathString carb.log_info(f"Physics scene prim path: {self._physics_scene_path}") break + if self._physics_scene_path is None: + raise RuntimeError("No physics scene found! Please make sure one exists.") return self._physics_scene_path @property diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/__init__.py index 851750f371..94b1245ab6 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/__init__.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/__init__.py @@ -61,3 +61,4 @@ class and the function call in a single line of code. from .sensors import * # noqa: F401, F403 from .shapes import * # noqa: F401, F403 from .spawner_cfg import * # noqa: F401, F403 +from .wrappers import * # noqa: F401, F403 diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/spawner_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/spawner_cfg.py index 089b38b29a..351b3cde96 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/spawner_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/spawner_cfg.py @@ -64,11 +64,6 @@ class SpawnerCfg: This parameter is only used when cloning prims. If False, then the asset will be inherited from the source prim, i.e. all USD changes to the source prim will be reflected in the cloned prims. - - .. versionadded:: 2023.1 - - This parameter is only supported from Isaac Sim 2023.1 onwards. If you are using an older - version of Isaac Sim, this parameter will be ignored. """ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/__init__.py new file mode 100644 index 0000000000..f05d3e58c7 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Sub-module for wrapping spawner configurations. + +Unlike the other spawner modules, this module provides a way to wrap multiple spawner configurations +into a single configuration. This is useful when the user wants to spawn multiple assets based on +different configurations. +""" + +from .wrappers import spawn_multi_asset, spawn_multi_usd_file +from .wrappers_cfg import MultiAssetSpawnerCfg, MultiUsdFileCfg diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/wrappers.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/wrappers.py new file mode 100644 index 0000000000..9040569e4a --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/wrappers.py @@ -0,0 +1,169 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import random +import re +from typing import TYPE_CHECKING + +import carb +import omni.isaac.core.utils.prims as prim_utils +import omni.isaac.core.utils.stage as stage_utils +from pxr import Sdf, Usd + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.sim.spawners.from_files import UsdFileCfg + +if TYPE_CHECKING: + from . import wrappers_cfg + + +def spawn_multi_asset( + prim_path: str, + cfg: wrappers_cfg.MultiAssetSpawnerCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Spawn multiple assets based on the provided configurations. + + This function spawns multiple assets based on the provided configurations. The assets are spawned + in the order they are provided in the list. If the :attr:`~MultiAssetSpawnerCfg.random_choice` parameter is + set to True, a random asset configuration is selected for each spawn. + + Args: + prim_path: The prim path to spawn the assets. + cfg: The configuration for spawning the assets. + translation: The translation of the spawned assets. Default is None. + orientation: The orientation of the spawned assets in (w, x, y, z) order. Default is None. + + Returns: + The created prim at the first prim path. + """ + # resolve: {SPAWN_NS}/AssetName + # note: this assumes that the spawn namespace already exists in the stage + root_path, asset_path = prim_path.rsplit("/", 1) + # check if input is a regex expression + # note: a valid prim path can only contain alphanumeric characters, underscores, and forward slashes + is_regex_expression = re.match(r"^[a-zA-Z0-9/_]+$", root_path) is None + + # resolve matching prims for source prim path expression + if is_regex_expression and root_path != "": + source_prim_paths = sim_utils.find_matching_prim_paths(root_path) + # if no matching prims are found, raise an error + if len(source_prim_paths) == 0: + raise RuntimeError( + f"Unable to find source prim path: '{root_path}'. Please create the prim before spawning." + ) + else: + source_prim_paths = [root_path] + + # find a free prim path to hold all the template prims + template_prim_path = stage_utils.get_next_free_path("/World/Template") + prim_utils.create_prim(template_prim_path, "Scope") + + # spawn everything first in a "Dataset" prim + proto_prim_paths = list() + for index, asset_cfg in enumerate(cfg.assets_cfg): + # append semantic tags if specified + if cfg.semantic_tags is not None: + if asset_cfg.semantic_tags is None: + asset_cfg.semantic_tags = cfg.semantic_tags + else: + asset_cfg.semantic_tags += cfg.semantic_tags + # override settings for properties + attr_names = ["mass_props", "rigid_props", "collision_props", "activate_contact_sensors", "deformable_props"] + for attr_name in attr_names: + attr_value = getattr(cfg, attr_name) + if hasattr(asset_cfg, attr_name) and attr_value is not None: + setattr(asset_cfg, attr_name, attr_value) + # spawn single instance + proto_prim_path = f"{template_prim_path}/Asset_{index:04d}" + asset_cfg.func(proto_prim_path, asset_cfg, translation=translation, orientation=orientation) + # append to proto prim paths + proto_prim_paths.append(proto_prim_path) + + # resolve prim paths for spawning and cloning + prim_paths = [f"{source_prim_path}/{asset_path}" for source_prim_path in source_prim_paths] + + # acquire stage + stage = stage_utils.get_current_stage() + + # manually clone prims if the source prim path is a regex expression + # note: unlike in the cloner API from Isaac Sim, we do not "reset" xforms on the copied prims. + # This is because the "spawn" calls during the creation of the proto prims already handles this operation. + with Sdf.ChangeBlock(): + for index, prim_path in enumerate(prim_paths): + # spawn single instance + env_spec = Sdf.CreatePrimInLayer(stage.GetRootLayer(), prim_path) + # randomly select an asset configuration + if cfg.random_choice: + proto_path = random.choice(proto_prim_paths) + else: + proto_path = proto_prim_paths[index % len(proto_prim_paths)] + # copy the proto prim + Sdf.CopySpec(env_spec.layer, Sdf.Path(proto_path), env_spec.layer, Sdf.Path(prim_path)) + + # delete the dataset prim after spawning + prim_utils.delete_prim(template_prim_path) + + # set carb setting to indicate Isaac Lab's environments that different prims have been spawned + # at varying prim paths. In this case, PhysX parser shouldn't optimize the stage parsing. + # the flag is mainly used to inform the user that they should disable `InteractiveScene.replicate_physics` + carb_settings_iface = carb.settings.get_settings() + carb_settings_iface.set_bool("/isaaclab/spawn/multi_assets", True) + + # return the prim + return prim_utils.get_prim_at_path(prim_paths[0]) + + +def spawn_multi_usd_file( + prim_path: str, + cfg: wrappers_cfg.MultiUsdFileCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Spawn multiple USD files based on the provided configurations. + + This function creates configuration instances corresponding the individual USD files and + calls the :meth:`spawn_multi_asset` method to spawn them into the scene. + + Args: + prim_path: The prim path to spawn the assets. + cfg: The configuration for spawning the assets. + translation: The translation of the spawned assets. Default is None. + orientation: The orientation of the spawned assets in (w, x, y, z) order. Default is None. + + Returns: + The created prim at the first prim path. + """ + # needed here to avoid circular imports + from .wrappers_cfg import MultiAssetSpawnerCfg + + # parse all the usd files + if isinstance(cfg.usd_path, str): + usd_paths = [cfg.usd_path] + else: + usd_paths = cfg.usd_path + + # make a template usd config + usd_template_cfg = UsdFileCfg() + for attr_name, attr_value in cfg.__dict__.items(): + # skip names we know are not present + if attr_name in ["func", "usd_path", "random_choice"]: + continue + # set the attribute into the template + setattr(usd_template_cfg, attr_name, attr_value) + + # create multi asset configuration of USD files + multi_asset_cfg = MultiAssetSpawnerCfg(assets_cfg=[]) + for usd_path in usd_paths: + usd_cfg = usd_template_cfg.replace(usd_path=usd_path) + multi_asset_cfg.assets_cfg.append(usd_cfg) + # set random choice + multi_asset_cfg.random_choice = cfg.random_choice + + # call the original function + return spawn_multi_asset(prim_path, multi_asset_cfg, translation, orientation) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/wrappers_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/wrappers_cfg.py new file mode 100644 index 0000000000..83d42cc4af --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/wrappers_cfg.py @@ -0,0 +1,67 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +from omni.isaac.lab.sim.spawners.from_files import UsdFileCfg +from omni.isaac.lab.sim.spawners.spawner_cfg import DeformableObjectSpawnerCfg, RigidObjectSpawnerCfg, SpawnerCfg +from omni.isaac.lab.utils import configclass + +from . import wrappers + + +@configclass +class MultiAssetSpawnerCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg): + """Configuration parameters for loading multiple assets from their individual configurations. + + Specifying values for any properties at the configuration level will override the settings of + individual assets' configuration. For instance if the attribute + :attr:`MultiAssetSpawnerCfg.mass_props` is specified, its value will overwrite the values of the + mass properties in each configuration inside :attr:`assets_cfg` (wherever applicable). + This is done to simplify configuring similar properties globally. By default, all properties are set to None. + + The following is an exception to the above: + + * :attr:`visible`: This parameter is ignored. Its value for the individual assets is used. + * :attr:`semantic_tags`: If specified, it will be appended to each individual asset's semantic tags. + + """ + + func = wrappers.spawn_multi_asset + + assets_cfg: list[SpawnerCfg] = MISSING + """List of asset configurations to spawn.""" + + random_choice: bool = True + """Whether to randomly select an asset configuration. Default is True. + + If False, the asset configurations are spawned in the order they are provided in the list. + If True, a random asset configuration is selected for each spawn. + """ + + +@configclass +class MultiUsdFileCfg(UsdFileCfg): + """Configuration parameters for loading multiple USD files. + + Specifying values for any properties at the configuration level is applied to all the assets + imported from their USD files. + + .. tip:: + It is recommended that all the USD based assets follow a similar prim-hierarchy. + + """ + + func = wrappers.spawn_multi_usd_file + + usd_path: str | list[str] = MISSING + """Path or a list of paths to the USD files to spawn asset from.""" + + random_choice: bool = True + """Whether to randomly select an asset configuration. Default is True. + + If False, the asset configurations are spawned in the order they are provided in the list. + If True, a random asset configuration is selected for each spawn. + """ diff --git a/source/extensions/omni.isaac.lab/test/sim/test_spawn_wrappers.py b/source/extensions/omni.isaac.lab/test/sim/test_spawn_wrappers.py new file mode 100644 index 0000000000..1260facf58 --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/sim/test_spawn_wrappers.py @@ -0,0 +1,191 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Launch Isaac Sim Simulator first.""" + +from omni.isaac.lab.app import AppLauncher, run_tests + +# launch omniverse app +simulation_app = AppLauncher(headless=True).app + +"""Rest everything follows.""" + +import unittest + +import omni.isaac.core.utils.prims as prim_utils +import omni.isaac.core.utils.stage as stage_utils +from omni.isaac.core.simulation_context import SimulationContext + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.utils.assets import ISAACLAB_NUCLEUS_DIR + + +class TestSpawningWrappers(unittest.TestCase): + """Test fixture for checking spawning of multiple assets wrappers.""" + + def setUp(self) -> None: + """Create a blank new stage for each test.""" + # Create a new stage + stage_utils.create_new_stage() + # Simulation time-step + self.dt = 0.1 + # Load kit helper + self.sim = SimulationContext(physics_dt=self.dt, rendering_dt=self.dt, backend="numpy") + # Wait for spawning + stage_utils.update_stage() + + def tearDown(self) -> None: + """Stops simulator after each test.""" + # stop simulation + self.sim.stop() + self.sim.clear() + self.sim.clear_all_callbacks() + self.sim.clear_instance() + + """ + Tests - Multiple assets. + """ + + def test_spawn_multiple_shapes_with_global_settings(self): + """Test spawning of shapes randomly with global rigid body settings.""" + # Define prim parents + num_clones = 10 + for i in range(num_clones): + prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + + # Spawn shapes + cfg = sim_utils.MultiAssetSpawnerCfg( + assets_cfg=[ + sim_utils.ConeCfg( + radius=0.3, + height=0.6, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2), + mass_props=sim_utils.MassPropertiesCfg(mass=100.0), # this one should get overridden + ), + sim_utils.CuboidCfg( + size=(0.3, 0.3, 0.3), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2), + ), + sim_utils.SphereCfg( + radius=0.3, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0), metallic=0.2), + ), + ], + random_choice=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + mass_props=sim_utils.MassPropertiesCfg(mass=1.0), + collision_props=sim_utils.CollisionPropertiesCfg(), + ) + prim = cfg.func("/World/env_.*/Cone", cfg) + + # Check validity + self.assertTrue(prim.IsValid()) + self.assertEqual(prim_utils.get_prim_path(prim), "/World/env_0/Cone") + # Find matching prims + prim_paths = prim_utils.find_matching_prim_paths("/World/env_*/Cone") + self.assertEqual(len(prim_paths), num_clones) + + # Check all prims have correct settings + for prim_path in prim_paths: + prim = prim_utils.get_prim_at_path(prim_path) + self.assertEqual(prim.GetAttribute("physics:mass").Get(), cfg.mass_props.mass) + + def test_spawn_multiple_shapes_with_individual_settings(self): + """Test spawning of shapes randomly with individual rigid object settings""" + # Define prim parents + num_clones = 10 + for i in range(num_clones): + prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + + # Make a list of masses + mass_variations = [2.0, 3.0, 4.0] + # Spawn shapes + cfg = sim_utils.MultiAssetSpawnerCfg( + assets_cfg=[ + sim_utils.ConeCfg( + radius=0.3, + height=0.6, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=mass_variations[0]), + collision_props=sim_utils.CollisionPropertiesCfg(), + ), + sim_utils.CuboidCfg( + size=(0.3, 0.3, 0.3), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=mass_variations[1]), + collision_props=sim_utils.CollisionPropertiesCfg(), + ), + sim_utils.SphereCfg( + radius=0.3, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0), metallic=0.2), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=mass_variations[2]), + collision_props=sim_utils.CollisionPropertiesCfg(), + ), + ], + random_choice=True, + ) + prim = cfg.func("/World/env_.*/Cone", cfg) + + # Check validity + self.assertTrue(prim.IsValid()) + self.assertEqual(prim_utils.get_prim_path(prim), "/World/env_0/Cone") + # Find matching prims + prim_paths = prim_utils.find_matching_prim_paths("/World/env_*/Cone") + self.assertEqual(len(prim_paths), num_clones) + + # Check all prims have correct settings + for prim_path in prim_paths: + prim = prim_utils.get_prim_at_path(prim_path) + self.assertTrue(prim.GetAttribute("physics:mass").Get() in mass_variations) + + """ + Tests - Multiple USDs. + """ + + def test_spawn_multiple_files_with_global_settings(self): + """Test spawning of files randomly with global articulation settings.""" + # Define prim parents + num_clones = 10 + for i in range(num_clones): + prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + + # Spawn shapes + cfg = sim_utils.MultiUsdFileCfg( + usd_path=[ + f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-C/anymal_c.usd", + f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-D/anymal_d.usd", + ], + random_choice=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + retain_accelerations=False, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=1000.0, + max_depenetration_velocity=1.0, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=True, solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + activate_contact_sensors=True, + ) + prim = cfg.func("/World/env_.*/Robot", cfg) + + # Check validity + self.assertTrue(prim.IsValid()) + self.assertEqual(prim_utils.get_prim_path(prim), "/World/env_0/Robot") + # Find matching prims + prim_paths = prim_utils.find_matching_prim_paths("/World/env_*/Robot") + self.assertEqual(len(prim_paths), num_clones) + + +if __name__ == "__main__": + run_tests() diff --git a/source/standalone/demos/multi_asset.py b/source/standalone/demos/multi_asset.py new file mode 100644 index 0000000000..6363999949 --- /dev/null +++ b/source/standalone/demos/multi_asset.py @@ -0,0 +1,244 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""This script demonstrates how to spawn multiple objects in multiple environments. + +.. code-block:: bash + + # Usage + ./isaaclab.sh -p source/standalone/demos/multi_asset.py --num_envs 2048 + +""" + +from __future__ import annotations + +"""Launch Isaac Sim Simulator first.""" + + +import argparse + +from omni.isaac.lab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Demo on spawning different objects in multiple environments.") +parser.add_argument("--num_envs", type=int, default=1024, help="Number of environments to spawn.") +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import random + +import omni.usd +from pxr import Gf, Sdf + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg +from omni.isaac.lab.scene import InteractiveScene, InteractiveSceneCfg +from omni.isaac.lab.sim import SimulationContext +from omni.isaac.lab.utils import Timer, configclass +from omni.isaac.lab.utils.assets import ISAACLAB_NUCLEUS_DIR + +## +# Pre-defined Configuration +## + +from omni.isaac.lab_assets.anymal import ANYDRIVE_3_LSTM_ACTUATOR_CFG # isort: skip + + +## +# Randomization events. +## + + +def randomize_shape_color(prim_path_expr: str): + """Randomize the color of the geometry.""" + # acquire stage + stage = omni.usd.get_context().get_stage() + # resolve prim paths for spawning and cloning + prim_paths = sim_utils.find_matching_prim_paths(prim_path_expr) + # manually clone prims if the source prim path is a regex expression + with Sdf.ChangeBlock(): + for prim_path in prim_paths: + # spawn single instance + prim_spec = Sdf.CreatePrimInLayer(stage.GetRootLayer(), prim_path) + + # DO YOUR OWN OTHER KIND OF RANDOMIZATION HERE! + # Note: Just need to acquire the right attribute about the property you want to set + # Here is an example on setting color randomly + color_spec = prim_spec.GetAttributeAtPath(prim_path + "/geometry/material/Shader.inputs:diffuseColor") + color_spec.default = Gf.Vec3f(random.random(), random.random(), random.random()) + + +## +# Scene Configuration +## + + +@configclass +class MultiObjectSceneCfg(InteractiveSceneCfg): + """Configuration for a multi-object scene.""" + + # ground plane + ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg()) + + # lights + dome_light = AssetBaseCfg( + prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + ) + + # rigid object + object: RigidObjectCfg = RigidObjectCfg( + prim_path="/World/envs/env_.*/Object", + spawn=sim_utils.MultiAssetSpawnerCfg( + assets_cfg=[ + sim_utils.ConeCfg( + radius=0.3, + height=0.6, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2), + ), + sim_utils.CuboidCfg( + size=(0.3, 0.3, 0.3), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2), + ), + sim_utils.SphereCfg( + radius=0.3, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0), metallic=0.2), + ), + ], + random_choice=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + mass_props=sim_utils.MassPropertiesCfg(mass=1.0), + collision_props=sim_utils.CollisionPropertiesCfg(), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 2.0)), + ) + + # articulation + robot: ArticulationCfg = ArticulationCfg( + prim_path="/World/envs/env_.*/Robot", + spawn=sim_utils.MultiUsdFileCfg( + usd_path=[ + f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-C/anymal_c.usd", + f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-D/anymal_d.usd", + ], + random_choice=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + retain_accelerations=False, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=1000.0, + max_depenetration_velocity=1.0, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=True, solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + activate_contact_sensors=True, + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.0, 0.6), + joint_pos={ + ".*HAA": 0.0, # all HAA + ".*F_HFE": 0.4, # both front HFE + ".*H_HFE": -0.4, # both hind HFE + ".*F_KFE": -0.8, # both front KFE + ".*H_KFE": 0.8, # both hind KFE + }, + ), + actuators={"legs": ANYDRIVE_3_LSTM_ACTUATOR_CFG}, + ) + + +## +# Simulation Loop +## + + +def run_simulator(sim: SimulationContext, scene: InteractiveScene): + """Runs the simulation loop.""" + # Extract scene entities + # note: we only do this here for readability. + rigid_object = scene["object"] + robot = scene["robot"] + # Define simulation stepping + sim_dt = sim.get_physics_dt() + count = 0 + # Simulation loop + while simulation_app.is_running(): + # Reset + if count % 500 == 0: + # reset counter + count = 0 + # reset the scene entities + # object + root_state = rigid_object.data.default_root_state.clone() + root_state[:, :3] += scene.env_origins + rigid_object.write_root_state_to_sim(root_state) + # robot + # -- root state + root_state = robot.data.default_root_state.clone() + root_state[:, :3] += scene.env_origins + robot.write_root_state_to_sim(root_state) + # -- joint state + joint_pos, joint_vel = robot.data.default_joint_pos.clone(), robot.data.default_joint_vel.clone() + robot.write_joint_state_to_sim(joint_pos, joint_vel) + # clear internal buffers + scene.reset() + print("[INFO]: Resetting scene state...") + + # Apply action to robot + robot.set_joint_position_target(robot.data.default_joint_pos) + # Write data to sim + scene.write_data_to_sim() + # Perform step + sim.step() + # Increment counter + count += 1 + # Update buffers + scene.update(sim_dt) + + +def main(): + """Main function.""" + # Load kit helper + sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device) + sim = SimulationContext(sim_cfg) + # Set main camera + sim.set_camera_view([2.5, 0.0, 4.0], [0.0, 0.0, 2.0]) + + # Design scene + scene_cfg = MultiObjectSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0, replicate_physics=False) + with Timer("[INFO] Time to create scene: "): + scene = InteractiveScene(scene_cfg) + + with Timer("[INFO] Time to randomize scene: "): + # DO YOUR OWN OTHER KIND OF RANDOMIZATION HERE! + # Note: Just need to acquire the right attribute about the property you want to set + # Here is an example on setting color randomly + randomize_shape_color(scene_cfg.object.prim_path) + + # Play the simulator + sim.reset() + # Now we are ready! + print("[INFO]: Setup complete...") + # Run the simulator + run_simulator(sim, scene) + + +if __name__ == "__main__": + # run the main execution + main() + # close sim app + simulation_app.close() diff --git a/tools/run_all_tests.py b/tools/run_all_tests.py index e3a73c71ef..733af27c02 100644 --- a/tools/run_all_tests.py +++ b/tools/run_all_tests.py @@ -231,14 +231,14 @@ def test_all( else: stdout_str = stdout.decode("utf-8") else: - stdout = "" + stdout_str = "" if stderr is not None: if isinstance(stderr, str): stderr_str = stderr else: stderr_str = stderr.decode("utf-8") else: - stderr = "" + stderr_str = "" # Write to log file logging.info(stdout_str)