diff --git a/.github/workflows/mirror-buildspec.yml b/.aws/mirror-buildspec.yml similarity index 100% rename from .github/workflows/mirror-buildspec.yml rename to .aws/mirror-buildspec.yml diff --git a/.github/workflows/postmerge-ci-buildspec.yml b/.aws/postmerge-ci-buildspec.yml similarity index 96% rename from .github/workflows/postmerge-ci-buildspec.yml rename to .aws/postmerge-ci-buildspec.yml index e071d7db09..9958483c07 100644 --- a/.github/workflows/postmerge-ci-buildspec.yml +++ b/.aws/postmerge-ci-buildspec.yml @@ -34,7 +34,7 @@ phases: 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_VERSION_ARG=$ISAACSIM_BASE_VERSION \ --build-arg ISAACSIM_ROOT_PATH_ARG=/isaac-sim \ --build-arg ISAACLAB_PATH_ARG=/workspace/isaaclab \ --build-arg DOCKER_USER_HOME_ARG=/root \ diff --git a/.github/workflows/premerge-ci-buildspec.yml b/.aws/premerge-ci-buildspec.yml similarity index 97% rename from .github/workflows/premerge-ci-buildspec.yml rename to .aws/premerge-ci-buildspec.yml index 4fa2372b4e..744f92786a 100644 --- a/.github/workflows/premerge-ci-buildspec.yml +++ b/.aws/premerge-ci-buildspec.yml @@ -69,7 +69,7 @@ phases: 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_VERSION_ARG=$ISAACSIM_BASE_VERSION \ --build-arg ISAACSIM_ROOT_PATH_ARG=/isaac-sim \ --build-arg ISAACLAB_PATH_ARG=/workspace/isaaclab \ --build-arg DOCKER_USER_HOME_ARG=/root \ diff --git a/.gitattributes b/.gitattributes index 37ab2e466a..65a6b946ab 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,3 +9,4 @@ *.mp4 filter=lfs diff=lfs merge=lfs -text *.pt filter=lfs diff=lfs merge=lfs -text *.jit filter=lfs diff=lfs merge=lfs -text +*.hdf5 filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 2465869832..51a9fbfd98 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,26 +1,34 @@ name: Build & deploy docs -on: [push] +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: check-secrets: name: Check secrets runs-on: ubuntu-latest outputs: - trigger-build: ${{ steps.trigger-build.outputs.defined }} + trigger-deploy: ${{ steps.trigger-deploy.outputs.defined }} steps: - - id: trigger-build - 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" + - 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 and deploy documentation + name: Build Docs runs-on: ubuntu-latest needs: [check-secrets] - if: needs.check-secrets.outputs.trigger-build == 'true' steps: - name: Checkout code @@ -36,12 +44,38 @@ jobs: working-directory: ./docs run: pip install -r requirements.txt - - name: Generate docs + - name: Check branch docs building + working-directory: ./docs + if: needs.check-secrets.outputs.trigger-deploy != 'true' + run: make current-docs + + - name: Generate multi-version docs working-directory: ./docs - run: make html + run: | + git fetch --prune --unshallow --tags + make multi-docs + + - name: Upload docs artifact + uses: actions/upload-artifact@v4 + with: + name: docs-html + path: ./docs/_build + + deploy-docs: + name: Deploy Docs + runs-on: ubuntu-latest + needs: [check-secrets, build-docs] + if: needs.check-secrets.outputs.trigger-deploy == 'true' + + steps: + - name: Download docs artifact + uses: actions/download-artifact@v4 + with: + name: docs-html + path: ./docs/_build - name: Deploy to gh-pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs/_build/html + publish_dir: ./docs/_build diff --git a/.vscode/tools/settings.template.json b/.vscode/tools/settings.template.json index 7d696c44b8..c794051784 100644 --- a/.vscode/tools/settings.template.json +++ b/.vscode/tools/settings.template.json @@ -1,4 +1,9 @@ { + "files.exclude": { + "**/.mypy_cache": true, + "**/__pycache__": true, + "**/*.egg-info": true + }, "files.associations": { "*.tpp": "cpp", "*.kit": "toml", @@ -40,7 +45,6 @@ "teleoperation", "xform", "numpy", - "tensordict", "flatcache", "physx", "dpad", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b75e4e5bc1..b837c0df92 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,4 +5,4 @@ the framework more mature and useful for everyone. These may happen in forms of design proposals and more. For general information on how to contribute see -. +. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 26503d4e06..9b3ea57d63 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -6,7 +6,7 @@ To see the full list of contributors, please check the revision history in the s Guidelines for modifications: -* Please keep the lists sorted alphabetically. +* Please keep the **lists sorted alphabetically**. * Names should be added to this file as: *individual names* or *organizations*. * E-mail addresses are tracked elsewhere to avoid spam. @@ -19,6 +19,7 @@ Guidelines for modifications: --- +* Antonio Serrano-Muñoz * David Hoeller * Farbod Farshidian * Hunter Hansen @@ -28,6 +29,7 @@ Guidelines for modifications: * Mayank Mittal * Nikita Rudin * Pascal Roth +* Sheikh Dawood ## Contributors @@ -35,38 +37,50 @@ Guidelines for modifications: * Amr Mousa * Andrej Orsula * Anton Bjørndahl Mortensen -* Antonio Serrano-Muñoz * Arjun Bhardwaj * Brayden Zhang * Calvin Yu * Chenyu Yang +* CY (Chien-Ying) Chen * David Yang +* Dorsa Rohani +* Felix Yu * Gary Lvov +* Giulio Romualdi +* Haoran Zhou * HoJin Jeon +* Jan Kerner * Jean Tampon * Jia Lin Yuan +* Jinghuan Shang * Jingzhou Liu * Johnson Sun * Kaixi Bao * Kourosh Darvish * Lionel Gulich +* Louis Le Lay * Lorenz Wellhausen * Masoud Moghani * Michael Gussert * Muhong Guo * Nuralem Abizov +* Oyindamola Omotuyi * Özhan Özen +* Peter Du +* Qian Wan * Qinxi Yu * René Zurbrügg * Ritvik Singh * Rosario Scalise +* Ryley McCarroll * Shafeef Omar * Vladimir Fokow +* Wei Yang * Xavier Nal * Yang Jin +* Yujian Zhang * Zhengyu Zhang * Ziqi Fan -* Qian Wan ## Acknowledgements diff --git a/README.md b/README.md index 590fd5aeb7..a46cd89d8f 100644 --- a/README.md +++ b/README.md @@ -13,23 +13,38 @@ [![License](https://img.shields.io/badge/license-BSD--3-yellow.svg)](https://opensource.org/licenses/BSD-3-Clause) -**Isaac Lab** is a unified and modular framework for robot learning that aims to simplify common workflows -in robotics research (such as RL, learning from demonstrations, and motion planning). It is built upon -[NVIDIA Isaac Sim](https://docs.omniverse.nvidia.com/isaacsim/latest/overview.html) to leverage the latest -simulation capabilities for photo-realistic scenes and fast and accurate simulation. +**Isaac Lab** is a GPU-accelerated, open-source framework designed to unify and simplify robotics research workflows, such as reinforcement learning, imitation learning, and motion planning. Built on [NVIDIA Isaac Sim](https://docs.omniverse.nvidia.com/isaacsim/latest/overview.html), it combines fast and accurate physics and sensor simulation, making it an ideal choice for sim-to-real transfer in robotics. + +Isaac Lab provides developers with a range of essential features for accurate sensor simulation, such as RTX-based cameras, LIDAR, or contact sensors. The framework's GPU acceleration enables users to run complex simulations and computations faster, which is key for iterative processes like reinforcement learning and data-intensive tasks. Moreover, Isaac Lab can run locally or be distributed across the cloud, offering flexibility for large-scale deployments. + +## Key Features + +Isaac Lab offers a comprehensive set of tools and environments designed to facilitate robot learning: +- **Robots**: A diverse collection of robots, from manipulators, quadrupeds, to humanoids, with 16 commonly available models. +- **Environments**: Ready-to-train implementations of more than 30 environments, which can be trained with popular reinforcement learning frameworks such as RSL RL, SKRL, RL Games, or Stable Baselines. We also support multi-agent reinforcement learning. +- **Physics**: Rigid bodies, articulated systems, deformable objects +- **Sensors**: RGB/depth/segmentation cameras, camera annotations, IMU, contact sensors, ray casters. + + +## Getting Started + +Our [documentation page](https://isaac-sim.github.io/IsaacLab) provides everything you need to get started, including detailed tutorials and step-by-step guides. Follow these links to learn more about: + +- [Installation steps](https://isaac-sim.github.io/IsaacLab/main/source/setup/installation/index.html#local-installation) +- [Reinforcement learning](https://isaac-sim.github.io/IsaacLab/main/source/overview/reinforcement-learning/rl_existing_scripts.html) +- [Tutorials](https://isaac-sim.github.io/IsaacLab/main/source/tutorials/index.html) +- [Available environments](https://isaac-sim.github.io/IsaacLab/main/source/overview/environments.html) -Please refer to our [documentation page](https://isaac-sim.github.io/IsaacLab) to learn more about the -installation steps, features, tutorials, and how to set up your project with Isaac Lab. ## Contributing to Isaac Lab We wholeheartedly welcome contributions from the community to make this framework mature and useful for everyone. These may happen as bug reports, feature requests, or code contributions. For details, please check our -[contribution guidelines](https://isaac-sim.github.io/IsaacLab/source/refs/contributing.html). +[contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html). ## Troubleshooting -Please see the [troubleshooting](https://isaac-sim.github.io/IsaacLab/source/refs/troubleshooting.html) section for +Please see the [troubleshooting](https://isaac-sim.github.io/IsaacLab/main/source/refs/troubleshooting.html) section for common fixes or [submit an issue](https://github.com/isaac-sim/IsaacLab/issues). For issues related to Isaac Sim, we recommend checking its [documentation](https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/overview.html) diff --git a/VERSION b/VERSION index 26aaba0e86..f0bb29e763 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.0 +1.3.0 diff --git a/docker/cluster/cluster_interface.sh b/docker/cluster/cluster_interface.sh index ec93eefee0..6684d835c9 100755 --- a/docker/cluster/cluster_interface.sh +++ b/docker/cluster/cluster_interface.sh @@ -16,8 +16,18 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" #== # Functions #== +# Function to display warnings in red +display_warning() { + echo -e "\033[31mWARNING: $1\033[0m" +} + +# Helper function to compare version numbers +version_gte() { + # Returns 0 if the first version is greater than or equal to the second, otherwise 1 + [ "$(printf '%s\n' "$1" "$2" | sort -V | head -n 1)" == "$2" ] +} + # Function to check docker versions -# If docker version is more than 25, the script errors out. check_docker_version() { # check if docker is installed if ! command -v docker &> /dev/null; then @@ -28,12 +38,17 @@ check_docker_version() { docker_version=$(docker --version | awk '{ print $3 }') apptainer_version=$(apptainer --version | awk '{ print $3 }') - # Check if version is above 25.xx - if [ "$(echo "${docker_version}" | cut -d '.' -f 1)" -ge 25 ]; then - echo "[ERROR]: Docker version ${docker_version} is not compatible with Apptainer version ${apptainer_version}. Exiting." - exit 1 + # Check if Docker version is exactly 24.0.7 or Apptainer version is exactly 1.2.5 + if [ "$docker_version" = "24.0.7" ] && [ "$apptainer_version" = "1.2.5" ]; then + echo "[INFO]: Docker version ${docker_version} and Apptainer version ${apptainer_version} are tested and compatible." + + # Check if Docker version is >= 27.0.0 and Apptainer version is >= 1.3.4 + elif version_gte "$docker_version" "27.0.0" && version_gte "$apptainer_version" "1.3.4"; then + echo "[INFO]: Docker version ${docker_version} and Apptainer version ${apptainer_version} are tested and compatible." + + # Else, display a warning for non-tested versions else - echo "[INFO]: Building singularity with docker version: ${docker_version} and Apptainer version: ${apptainer_version}." + display_warning "Docker version ${docker_version} and Apptainer version ${apptainer_version} are non-tested versions. There could be issues, please try to update them. More info: https://isaac-sim.github.io/IsaacLab/source/deployment/cluster.html" fi } @@ -62,11 +77,9 @@ submit_job() { case $CLUSTER_JOB_SCHEDULER in "SLURM") - CMD=sbatch job_script_file=submit_job_slurm.sh ;; "PBS") - CMD=bash job_script_file=submit_job_pbs.sh ;; *) @@ -75,7 +88,7 @@ submit_job() { ;; esac - ssh $CLUSTER_LOGIN "cd $CLUSTER_ISAACLAB_DIR && $CMD $CLUSTER_ISAACLAB_DIR/docker/cluster/$job_script_file \"$CLUSTER_ISAACLAB_DIR\" \"isaac-lab-$profile\" ${@}" + ssh $CLUSTER_LOGIN "cd $CLUSTER_ISAACLAB_DIR && bash $CLUSTER_ISAACLAB_DIR/docker/cluster/$job_script_file \"$CLUSTER_ISAACLAB_DIR\" \"isaac-lab-$profile\" ${@}" } #== @@ -141,7 +154,7 @@ case $command in fi # Check if Docker image exists check_image_exists isaac-lab-$profile:latest - # Check if Docker version is greater than 25 + # Check docker and apptainer version check_docker_version # source env file to get cluster login and path information source $SCRIPT_DIR/.env.cluster @@ -162,11 +175,17 @@ case $command in scp $SCRIPT_DIR/exports/isaac-lab-$profile.tar $CLUSTER_LOGIN:$CLUSTER_SIF_PATH/isaac-lab-$profile.tar ;; job) - [ $# -ge 1 ] && profile=$1 && shift + if [ $# -ge 1 ]; then + passed_profile=$1 + if [ -f "$SCRIPT_DIR/../.env.$passed_profile" ]; then + profile=$passed_profile + shift + fi + fi job_args="$@" - echo "Executing job command" - [ -n "$profile" ] && echo "Using profile: $profile" - [ -n "$job_args" ] && echo "Job arguments: $job_args" + echo "[INFO] Executing job command" + [ -n "$profile" ] && echo -e "\tUsing profile: $profile" + [ -n "$job_args" ] && echo -e "\tJob arguments: $job_args" source $SCRIPT_DIR/.env.cluster # Get current date and time current_datetime=$(date +"%Y%m%d_%H%M%S") diff --git a/docker/utils/container_interface.py b/docker/utils/container_interface.py index c717457029..aacbc79ccd 100644 --- a/docker/utils/container_interface.py +++ b/docker/utils/container_interface.py @@ -142,8 +142,7 @@ def enter(self): "exec", "--interactive", "--tty", - "-e", - f"DISPLAY={os.environ['DISPLAY']}", + *(["-e", f"DISPLAY={os.environ['DISPLAY']}"] if "DISPLAY" in os.environ else []), f"{self.container_name}", "bash", ]) diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb9e..ce33dad503 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -8,13 +8,11 @@ SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +.PHONY: multi-docs +multi-docs: + @sphinx-multiversion "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) + @cp _redirect/index.html $(BUILDDIR)/index.html -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +.PHONY: current-docs +current-docs: + @$(SPHINXBUILD) "$(SOURCEDIR)" "$(BUILDDIR)/current" $(SPHINXOPTS) diff --git a/docs/README.md b/docs/README.md index c154e0ad0f..69a77a48d9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,75 @@ # Building Documentation -We use [Sphinx](https://www.sphinx-doc.org/en/master/) with the [Book Theme](https://sphinx-book-theme.readthedocs.io/en/stable/) for maintaining the documentation. +We use [Sphinx](https://www.sphinx-doc.org/en/master/) with the [Book Theme](https://sphinx-book-theme.readthedocs.io/en/stable/) for maintaining and generating our documentation. -> **Note:** To build the documentation, we recommend creating a virtual environment to avoid any conflicts with system installed dependencies. +> **Note:** To avoid dependency conflicts, we strongly recommend using a Python virtual environment to isolate the required dependencies from your system's global Python environment. -Execute the following instructions to build the documentation (assumed from the top of the repository): +## Current-Version Documentation -1. Install the dependencies for [Sphinx](https://www.sphinx-doc.org/en/master/): +This section describes how to build the documentation for the current version of the project. - ```bash - # enter the location where this readme exists - cd docs - # install dependencies - pip install -r requirements.txt - ``` +
+Linux -2. Generate the documentation file via: +```bash +# 1. Navigate to the docs directory and install dependencies +cd docs +pip install -r requirements.txt - ```bash - # make the html version - make html - ``` +# 2. Build the current documentation +make current-docs -3. The documentation is now available at `docs/_build/html/index.html`: +# 3. Open the current docs +xdg-open _build/current/index.html +``` +
- ```bash - # open on default browser - xdg-open _build/html/index.html - ``` +
Windows + +```batch +:: 1. Navigate to the docs directory and install dependencies +cd docs +pip install -r requirements.txt + +:: 2. Build the current documentation +make current-docs + +:: 3. Open the current docs +start _build\current\index.html +``` +
+ + +## Multi-Version Documentation + +This section describes how to build the multi-version documentation, which includes previous tags and the main branch. + +
Linux + +```bash +# 1. Navigate to the docs directory and install dependencies +cd docs +pip install -r requirements.txt + +# 2. Build the multi-version documentation +make multi-docs + +# 3. Open the multi-version docs +xdg-open _build/index.html +``` +
+ +
Windows + +```batch +:: 1. Navigate to the docs directory and install dependencies +cd docs +pip install -r requirements.txt + +:: 2. Build the multi-version documentation +make multi-docs + +:: 3. Open the multi-version docs +start _build\index.html +``` +
diff --git a/docs/_redirect/index.html b/docs/_redirect/index.html new file mode 100644 index 0000000000..5208597ed1 --- /dev/null +++ b/docs/_redirect/index.html @@ -0,0 +1,8 @@ + + + + Redirecting to the latest Isaac Lab documentation + + + + diff --git a/docs/_templates/versioning.html b/docs/_templates/versioning.html new file mode 100644 index 0000000000..eb67be60e1 --- /dev/null +++ b/docs/_templates/versioning.html @@ -0,0 +1,21 @@ +{% if versions %} + +{% endif %} diff --git a/docs/conf.py b/docs/conf.py index ca1fa9a075..0fccd611de 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,6 +54,8 @@ "sphinxcontrib.icon", "sphinx_copybutton", "sphinx_design", + "sphinx_tabs.tabs", # backwards compatibility for building docs on v1.0.0 + "sphinx_multiversion", ] # mathjax hacks @@ -115,7 +117,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "README.md", "licenses/*"] +exclude_patterns = ["_build", "_redirect", "_templates", "Thumbs.db", ".DS_Store", "README.md", "licenses/*"] # Mock out modules that are not available on RTD autodoc_mock_imports = [ @@ -127,6 +129,7 @@ "warp", "pxr", "omni.kit", + "omni.log", "omni.usd", "omni.client", "omni.physx", @@ -189,7 +192,7 @@ import sphinx_book_theme -html_title = "Isaac Lab documentation" +html_title = "Isaac Lab Documentation" html_theme_path = [sphinx_book_theme.get_html_theme_path()] html_theme = "sphinx_book_theme" html_favicon = "source/_static/favicon.ico" @@ -212,7 +215,7 @@ "show_toc_level": 1, "use_sidenotes": True, "logo": { - "text": "Isaac Lab documentation", + "text": "Isaac Lab Documentation", "image_light": "source/_static/NVIDIA-logo-white.png", "image_dark": "source/_static/NVIDIA-logo-black.png", }, @@ -239,7 +242,19 @@ "icon_links_label": "Quick Links", } -html_sidebars = {"**": ["navbar-logo.html", "icon-links.html", "search-field.html", "sbt-sidebar-nav.html"]} +templates_path = [ + "_templates", +] + +# Whitelist pattern for remotes +smv_remote_whitelist = r"^.*$" +# Whitelist pattern for branches (set to None to ignore all branches) +smv_branch_whitelist = os.getenv("SMV_BRANCH_WHITELIST", r"^(main|devel)$") +# Whitelist pattern for tags (set to None to ignore all tags) +smv_tag_whitelist = os.getenv("SMV_TAG_WHITELIST", r"^v[1-9]\d*\.\d+\.\d+$") +html_sidebars = { + "**": ["navbar-logo.html", "versioning.html", "icon-links.html", "search-field.html", "sbt-sidebar-nav.html"] +} # -- Advanced configuration ------------------------------------------------- @@ -247,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/index.rst b/docs/index.rst index 873ca677cd..7acde411ec 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -74,7 +74,9 @@ Table of Contents :maxdepth: 2 :caption: Getting Started + source/setup/ecosystem source/setup/installation/index + source/setup/installation/cloud_installation source/setup/faq .. toctree:: @@ -84,6 +86,7 @@ Table of Contents source/overview/developer-guide/index source/overview/core-concepts/index + source/overview/sensors/index source/overview/environments source/overview/reinforcement-learning/index source/overview/teleop_imitation @@ -96,9 +99,8 @@ Table of Contents source/features/hydra source/features/multi_gpu - source/features/tiled_rendering + Tiled Rendering source/features/reproducibility - .. source/features/motion_generators .. toctree:: :maxdepth: 1 @@ -128,6 +130,7 @@ Table of Contents :maxdepth: 1 :caption: References + source/refs/reference_architecture/index source/refs/additional_resources source/refs/contributing source/refs/troubleshooting diff --git a/docs/licenses/dependencies/einops-license.txt b/docs/licenses/dependencies/einops-license.txt new file mode 100644 index 0000000000..3a654e9066 --- /dev/null +++ b/docs/licenses/dependencies/einops-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Alex Rogozhnikov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/licenses/dependencies/sphinx-multiversion-license.txt b/docs/licenses/dependencies/sphinx-multiversion-license.txt new file mode 100644 index 0000000000..172d6b3f5d --- /dev/null +++ b/docs/licenses/dependencies/sphinx-multiversion-license.txt @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2020, Jan Holthuis +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/transformers-license.txt b/docs/licenses/dependencies/transformers-license.txt new file mode 100644 index 0000000000..68b7d66c97 --- /dev/null +++ b/docs/licenses/dependencies/transformers-license.txt @@ -0,0 +1,203 @@ +Copyright 2018- The Hugging Face team. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/make.bat b/docs/make.bat index 2119f51099..cdaf22f257 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -2,34 +2,63 @@ pushd %~dp0 -REM Command file for Sphinx documentation +REM Command file to build Sphinx documentation -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) set SOURCEDIR=. set BUILDDIR=_build -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 +REM Check if a specific target was passed +if "%1" == "multi-docs" ( + REM Check if SPHINXBUILD is set, if not default to sphinx-multiversion + if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-multiversion + ) + %SPHINXBUILD% >NUL 2>NUL + if errorlevel 9009 ( + echo. + echo.The 'sphinx-multiversion' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-multiversion' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 + ) + %SPHINXBUILD% %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + + REM Copy the redirect index.html to the build directory + copy _redirect\index.html %BUILDDIR%\index.html + goto end ) -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end +if "%1" == "current-docs" ( + REM Check if SPHINXBUILD is set, if not default to sphinx-build + if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build + ) + %SPHINXBUILD% >NUL 2>NUL + if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 + ) + %SPHINXBUILD% %SOURCEDIR% %BUILDDIR%\current %SPHINXOPTS% %O% + goto end +) -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +REM If no valid target is passed, show usage instructions +echo. +echo.Usage: +echo. make.bat multi-docs - To build the multi-version documentation. +echo. make.bat current-docs - To build the current documentation. +echo. :end popd diff --git a/docs/requirements.txt b/docs/requirements.txt index 33917bd712..13b2bfe9d6 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -7,6 +7,8 @@ sphinx-copybutton sphinx-icon sphinx_design sphinxemoji +sphinx-tabs # backwards compatibility for building docs on v1.0.0 +sphinx-multiversion==0.2.4 # basic python numpy diff --git a/docs/source/_static/demos/multi_asset.jpg b/docs/source/_static/demos/multi_asset.jpg new file mode 100644 index 0000000000..a59388532b Binary files /dev/null and b/docs/source/_static/demos/multi_asset.jpg differ diff --git a/docs/source/_static/overview/overview_sensors_contact_diagram.png b/docs/source/_static/overview/overview_sensors_contact_diagram.png new file mode 100644 index 0000000000..91b05c51fa Binary files /dev/null and b/docs/source/_static/overview/overview_sensors_contact_diagram.png differ diff --git a/docs/source/_static/overview/overview_sensors_depth.png b/docs/source/_static/overview/overview_sensors_depth.png new file mode 100644 index 0000000000..5437e0bf8d Binary files /dev/null and b/docs/source/_static/overview/overview_sensors_depth.png differ diff --git a/docs/source/_static/overview/overview_sensors_frame_transformer.png b/docs/source/_static/overview/overview_sensors_frame_transformer.png new file mode 100644 index 0000000000..c16944060e Binary files /dev/null and b/docs/source/_static/overview/overview_sensors_frame_transformer.png differ diff --git a/docs/source/_static/overview/overview_sensors_ft_visualizer.png b/docs/source/_static/overview/overview_sensors_ft_visualizer.png new file mode 100644 index 0000000000..c16ea4aa05 Binary files /dev/null and b/docs/source/_static/overview/overview_sensors_ft_visualizer.png differ diff --git a/docs/source/_static/overview/overview_sensors_instance.png b/docs/source/_static/overview/overview_sensors_instance.png new file mode 100644 index 0000000000..8683e71946 Binary files /dev/null and b/docs/source/_static/overview/overview_sensors_instance.png differ diff --git a/docs/source/_static/overview/overview_sensors_instanceID.png b/docs/source/_static/overview/overview_sensors_instanceID.png new file mode 100644 index 0000000000..2a3d4d9036 Binary files /dev/null and b/docs/source/_static/overview/overview_sensors_instanceID.png differ diff --git a/docs/source/_static/overview/overview_sensors_normals.png b/docs/source/_static/overview/overview_sensors_normals.png new file mode 100644 index 0000000000..6e3ab7f3cd Binary files /dev/null and b/docs/source/_static/overview/overview_sensors_normals.png differ diff --git a/docs/source/_static/overview/overview_sensors_rc_patterns.png b/docs/source/_static/overview/overview_sensors_rc_patterns.png new file mode 100644 index 0000000000..fc3c001b14 Binary files /dev/null and b/docs/source/_static/overview/overview_sensors_rc_patterns.png differ diff --git a/docs/source/_static/overview/overview_sensors_rc_visualizer.png b/docs/source/_static/overview/overview_sensors_rc_visualizer.png new file mode 100644 index 0000000000..ac4a06595e Binary files /dev/null and b/docs/source/_static/overview/overview_sensors_rc_visualizer.png differ diff --git a/docs/source/_static/overview/overview_sensors_rgb.png b/docs/source/_static/overview/overview_sensors_rgb.png new file mode 100644 index 0000000000..2f17e81039 Binary files /dev/null and b/docs/source/_static/overview/overview_sensors_rgb.png differ diff --git a/docs/source/_static/overview/overview_sensors_semantic.png b/docs/source/_static/overview/overview_sensors_semantic.png new file mode 100644 index 0000000000..aa14f16e15 Binary files /dev/null and b/docs/source/_static/overview/overview_sensors_semantic.png differ diff --git a/docs/source/_static/reference-architecture/deployment-dark.svg b/docs/source/_static/reference-architecture/deployment-dark.svg new file mode 100644 index 0000000000..ba8c739724 --- /dev/null +++ b/docs/source/_static/reference-architecture/deployment-dark.svg @@ -0,0 +1,3 @@ + + +
Robot Hardware
NVIDIA Isaac Perceptor
Custom Estimator
Isaac ROS Packages
State Estimator
Extract Observation
Actions Controller
Trained Model (.onnx, .pt)
Commanded Actions
Model Inference Runtime
Scaled Actions
1
2
3
4
5
6
7
diff --git a/docs/source/_static/reference-architecture/deployment-light.svg b/docs/source/_static/reference-architecture/deployment-light.svg new file mode 100644 index 0000000000..33857dbdbe --- /dev/null +++ b/docs/source/_static/reference-architecture/deployment-light.svg @@ -0,0 +1,3 @@ + + +
Robot Hardware
NVIDIA Isaac Perceptor
Custom Estimator
Isaac ROS Packages
State Estimator
Extract Observation
Actions Controller
Trained Model (.onnx, .pt)
Commanded Actions
Model Inference Runtime
Scaled Actions
1
2
3
4
5
6
7
diff --git a/docs/source/_static/reference-architecture/isaac-lab-ra-dark.svg b/docs/source/_static/reference-architecture/isaac-lab-ra-dark.svg new file mode 100644 index 0000000000..90ed29a93e --- /dev/null +++ b/docs/source/_static/reference-architecture/isaac-lab-ra-dark.svg @@ -0,0 +1,3 @@ + + +
Scene Assets
Robot Assets (.usd, .urdf)
Asset Input
Design Robot Learning Task
Scene Configuration
Asset Configuration
Configuration
Register Environment with Gymnasium
Learning Framework Wrapper
Video Wrapper
Wrap Environment
Wrapper API
Test Model
Multi-Node Training
Single GPU Training
Cloud-based Training
Multi-GPU Training
Run Training
diff --git a/docs/source/_static/reference-architecture/isaac-lab-ra-light.svg b/docs/source/_static/reference-architecture/isaac-lab-ra-light.svg new file mode 100644 index 0000000000..a58a6af1ec --- /dev/null +++ b/docs/source/_static/reference-architecture/isaac-lab-ra-light.svg @@ -0,0 +1,3 @@ + + +
Scene Assets
Robot Assets (.usd, .urdf)
Asset Input
Design Robot Learning Task
Scene Configuration
Asset Configuration
Configuration
Register Environment with Gymnasium
Learning Framework Wrapper
Video Wrapper
Wrap Environment
Wrapper API
Test Model
Multi-Node Training
Single GPU Training
Cloud-based Training
Multi-GPU Training
Run Training
diff --git a/docs/source/_static/reference-architecture/multi-gpu-training-dark.svg b/docs/source/_static/reference-architecture/multi-gpu-training-dark.svg new file mode 100644 index 0000000000..dd63d5769c --- /dev/null +++ b/docs/source/_static/reference-architecture/multi-gpu-training-dark.svg @@ -0,0 +1,3 @@ + + +
Environment 0
Gradients
Global Network
Updated Model
GPU 0
Learner 0
Environment 1
GPU 1
Learner 1
Environment N
GPU N
Learner N
diff --git a/docs/source/_static/reference-architecture/multi-gpu-training-light.svg b/docs/source/_static/reference-architecture/multi-gpu-training-light.svg new file mode 100644 index 0000000000..a279bff5e8 --- /dev/null +++ b/docs/source/_static/reference-architecture/multi-gpu-training-light.svg @@ -0,0 +1,3 @@ + + +
Environment 0
Gradients
Global Network
Updated Model
GPU 0
Learner 0
Environment 1
GPU 1
Learner 1
Environment N
GPU N
Learner N
diff --git a/docs/source/_static/reference-architecture/single-gpu-training-dark.svg b/docs/source/_static/reference-architecture/single-gpu-training-dark.svg new file mode 100644 index 0000000000..69f8e0f3c0 --- /dev/null +++ b/docs/source/_static/reference-architecture/single-gpu-training-dark.svg @@ -0,0 +1,3 @@ + + +
Isaac Lab
2
States
Add Noise
Assets
Rendering
Trained Model (.pt, .onnx)
Isaac Sim
1
Physics Sim
4
5
RL Libraries
3
Policy
Actions
Observations
6
diff --git a/docs/source/_static/reference-architecture/single-gpu-training-light.svg b/docs/source/_static/reference-architecture/single-gpu-training-light.svg new file mode 100644 index 0000000000..7463c470d8 --- /dev/null +++ b/docs/source/_static/reference-architecture/single-gpu-training-light.svg @@ -0,0 +1,3 @@ + + +
Isaac Lab
2
States
Add Noise
Assets
Rendering
Trained Model (.pt, .onnx)
Isaac Sim
1
Physics Sim
4
5
RL Libraries
3
Policy
Actions
Observations
6
diff --git a/docs/source/_static/refs.bib b/docs/source/_static/refs.bib index e7f82d17da..c87c90c076 100644 --- a/docs/source/_static/refs.bib +++ b/docs/source/_static/refs.bib @@ -139,3 +139,18 @@ @article{mittal2023orbit pages={3740-3747}, doi={10.1109/LRA.2023.3270034} } + +@article{shang2024theia, + title={Theia: Distilling diverse vision foundation models for robot learning}, + author={Shang, Jinghuan and Schmeckpeper, Karl and May, Brandon B and Minniti, Maria Vittoria and Kelestemur, Tarik and Watkins, David and Herlant, Laura}, + journal={arXiv preprint arXiv:2407.20179}, + year={2024} +} + +@inproceedings{he2016deep, + title={Deep residual learning for image recognition}, + author={He, Kaiming and Zhang, Xiangyu and Ren, Shaoqing and Sun, Jian}, + booktitle={Proceedings of the IEEE conference on computer vision and pattern recognition}, + pages={770--778}, + year={2016} +} diff --git a/docs/source/_static/setup/ecosystem-dark.jpg b/docs/source/_static/setup/ecosystem-dark.jpg new file mode 100644 index 0000000000..9057aeb48b Binary files /dev/null and b/docs/source/_static/setup/ecosystem-dark.jpg differ diff --git a/docs/source/_static/setup/ecosystem-light.jpg b/docs/source/_static/setup/ecosystem-light.jpg new file mode 100644 index 0000000000..5c2bbf5fe1 Binary files /dev/null and b/docs/source/_static/setup/ecosystem-light.jpg differ diff --git a/docs/source/_static/setup/verify_install.jpg b/docs/source/_static/setup/verify_install.jpg new file mode 100644 index 0000000000..166840dc70 Binary files /dev/null and b/docs/source/_static/setup/verify_install.jpg differ diff --git a/docs/source/_static/task-workflows/direct-based-dark.svg b/docs/source/_static/task-workflows/direct-based-dark.svg new file mode 100644 index 0000000000..2709dae140 --- /dev/null +++ b/docs/source/_static/task-workflows/direct-based-dark.svg @@ -0,0 +1,3 @@ + + +
Sensors
Observations
Apply Actions
Define Task
Define Step
Environment Scripting
NVIDIA
Isaac Sim
Perform Resets
Rewards
Commands
Learning Agent
Scene Creation
Articulation
Objects
Compute Signals
Actions
Perform
Randomization
diff --git a/docs/source/_static/task-workflows/direct-based-light.svg b/docs/source/_static/task-workflows/direct-based-light.svg new file mode 100644 index 0000000000..b39bffa353 --- /dev/null +++ b/docs/source/_static/task-workflows/direct-based-light.svg @@ -0,0 +1,3 @@ + + +
Sensors
Observations
Apply Actions
Define Task
Define Step
Environment Scripting
NVIDIA
Isaac Sim
Perform Resets
Rewards
Commands
Learning Agent
Scene Creation
Articulation
Objects
Compute Signals
Actions
Perform
Randomization
diff --git a/docs/source/_static/task-workflows/manager-based-dark.svg b/docs/source/_static/task-workflows/manager-based-dark.svg new file mode 100644 index 0000000000..b66ba1bfee --- /dev/null +++ b/docs/source/_static/task-workflows/manager-based-dark.svg @@ -0,0 +1,3 @@ + + +
Action Manager
Task-space
Joint-space
Termination Manager
Reward Manager
Curriculum Manager
Learning
Agent
Custom
Command Manager
Custom
Velocity
Pose
Observation Manager
Proprioception
Custom
Exteroception
NVIDIA
Isaac Sim
Articulation
Objects
Sensors
Interactive Scene
Event Manager
External Disturbances
Domain Randomization
diff --git a/docs/source/_static/task-workflows/manager-based-light.svg b/docs/source/_static/task-workflows/manager-based-light.svg new file mode 100644 index 0000000000..60fe6119d4 --- /dev/null +++ b/docs/source/_static/task-workflows/manager-based-light.svg @@ -0,0 +1,3 @@ + + +
Action Manager
Task-space
Joint-space
Termination Manager
Reward Manager
Curriculum Manager
Learning
Agent
Custom
Command Manager
Custom
Velocity
Pose
Observation Manager
Proprioception
Custom
Exteroception
NVIDIA
Isaac Sim
Articulation
Objects
Sensors
Interactive Scene
Event Manager
External Disturbances
Domain Randomization
diff --git a/docs/source/_static/tasks/manipulation/franka_stack.jpg b/docs/source/_static/tasks/manipulation/franka_stack.jpg new file mode 100644 index 0000000000..1da36113ec Binary files /dev/null and b/docs/source/_static/tasks/manipulation/franka_stack.jpg differ diff --git a/docs/source/api/lab/omni.isaac.lab.assets.rst b/docs/source/api/lab/omni.isaac.lab.assets.rst index d7dcfbac19..ba139a2395 100644 --- a/docs/source/api/lab/omni.isaac.lab.assets.rst +++ b/docs/source/api/lab/omni.isaac.lab.assets.rst @@ -12,6 +12,9 @@ RigidObject RigidObjectData RigidObjectCfg + RigidObjectCollection + RigidObjectCollectionData + RigidObjectCollectionCfg Articulation ArticulationData ArticulationCfg @@ -51,6 +54,26 @@ Rigid Object :show-inheritance: :exclude-members: __init__, class_type +Rigid Object Collection +----------------------- + +.. autoclass:: RigidObjectCollection + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: RigidObjectCollectionData + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__ + +.. autoclass:: RigidObjectCollectionCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + Articulation ------------ diff --git a/docs/source/api/lab/omni.isaac.lab.sensors.rst b/docs/source/api/lab/omni.isaac.lab.sensors.rst index 9eac25b797..3a82a42904 100644 --- a/docs/source/api/lab/omni.isaac.lab.sensors.rst +++ b/docs/source/api/lab/omni.isaac.lab.sensors.rst @@ -31,6 +31,8 @@ RayCasterCfg RayCasterCamera RayCasterCameraCfg + Imu + ImuCfg Sensor Base ----------- @@ -150,3 +152,17 @@ Ray-Cast Camera :inherited-members: :show-inheritance: :exclude-members: __init__, class_type + +Inertia Measurement Unit +------------------------ + +.. autoclass:: Imu + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: ImuCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type diff --git a/docs/source/api/lab/omni.isaac.lab.sim.rst b/docs/source/api/lab/omni.isaac.lab.sim.rst index 4e37e33b4a..b2b582c68b 100644 --- a/docs/source/api/lab/omni.isaac.lab.sim.rst +++ b/docs/source/api/lab/omni.isaac.lab.sim.rst @@ -19,6 +19,7 @@ SimulationContext SimulationCfg PhysxCfg + RenderCfg .. rubric:: Functions @@ -46,6 +47,11 @@ Simulation Configuration :show-inheritance: :exclude-members: __init__ +.. autoclass:: RenderCfg + :members: + :show-inheritance: + :exclude-members: __init__ + Simulation Context Builder -------------------------- 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/api/lab/omni.isaac.lab.utils.rst b/docs/source/api/lab/omni.isaac.lab.utils.rst index 2143ecfc28..c14ae19b6c 100644 --- a/docs/source/api/lab/omni.isaac.lab.utils.rst +++ b/docs/source/api/lab/omni.isaac.lab.utils.rst @@ -18,6 +18,7 @@ noise string timer + types warp .. Rubric:: Functions @@ -123,6 +124,13 @@ Timer operations :members: :show-inheritance: +Type operations +~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.types + :members: + :show-inheritance: + Warp operations ~~~~~~~~~~~~~~~ diff --git a/docs/source/deployment/cluster.rst b/docs/source/deployment/cluster.rst index beae7ef438..467fda90f4 100644 --- a/docs/source/deployment/cluster.rst +++ b/docs/source/deployment/cluster.rst @@ -45,15 +45,13 @@ development machine and the cluster. Such a connection will simplify the file tr the user cluster password from being requested multiple times. .. attention:: - The workflow has been tested with ``apptainer version 1.2.5-1.el7`` and ``docker version 24.0.7``. + The workflow has been tested with: - - ``apptainer``: - There have been reported binding issues with previous versions (such as ``apptainer version 1.1.3-1.el7``). Please - ensure that you are using the latest version. - - ``Docker``: - The latest versions (``25.x``) cannot be used as they are not compatible yet with apptainer/ singularity. + - ``apptainer version 1.2.5-1.el7`` and ``docker version 24.0.7`` + - ``apptainer version 1.3.4`` and ``docker version 27.3.1`` + + In the case of issues, please try to switch to those versions. - We are waiting for an update from the apptainer team. To track this issue, please check the `forum post`_. Configuring the cluster parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -211,4 +209,3 @@ The above will, in addition, also render videos of the training progress and sto .. _apptainer: https://apptainer.org/ .. _documentation: https://www.apptainer.org/docs/admin/main/installation.html#install-ubuntu-packages .. _SLURM documentation: https://www.slurm.schedmd.com/sbatch.html -.. _forum post: https://forums.docker.com/t/trouble-after-upgrade-to-docker-ce-25-0-1-on-debian-12/139613 diff --git a/docs/source/features/hydra.rst b/docs/source/features/hydra.rst index 2fa5d2d706..577eb7a6ec 100644 --- a/docs/source/features/hydra.rst +++ b/docs/source/features/hydra.rst @@ -89,8 +89,8 @@ Elements in dictionaries are handled as a parameters in the hierarchy. For examp .. literalinclude:: ../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py :language: python - :lines: 99-111 - :emphasize-lines: 10 + :lines: 90-114 + :emphasize-lines: 11 the ``position_range`` parameter can be modified with ``env.events.reset_cart_position.params.position_range="[-2.0, 2.0]"``. This example shows two noteworthy points: @@ -112,10 +112,10 @@ For example, for the configuration of the Cartpole camera depth environment: :language: python :start-at: class CartpoleDepthCameraEnvCfg :end-at: tiled_camera.width - :emphasize-lines: 16 + :emphasize-lines: 10, 15 If the user were to modify the width of the camera, i.e. ``env.tiled_camera.width=128``, then the parameter -``env.num_observations=10240`` (1*80*128) must be updated and given as input as well. +``env.observation_space=[80,128,1]`` must be updated and given as input as well. Similarly, the ``__post_init__`` method is not updated with the command line inputs. In the ``LocomotionVelocityRoughEnvCfg``, for example, the post init update is as follows: 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..fe141981fc --- /dev/null +++ b/docs/source/how-to/multi_asset_spawning.rst @@ -0,0 +1,129 @@ + +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, we also support multi-asset spawning with two mechanisms: + +1. Rigid object collections. This allows the user to spawn multiple rigid objects in each environment and access/modify + them with a unified API, improving performance. + +2. Spawning different assets under the same prim path. This allows the user to create diverse simulations, where each + environment has a different asset. + +This guide describes how to use these two mechanisms. + +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: 109-131, 135-179, 184-203 + :linenos: + +This script creates multiple environments, where each environment has: + +* a rigid object collection containing a cone, a cube, and a sphere +* a rigid object that is either a cone, a cube, or a sphere, chosen at random +* an articulation that is either the ANYmal-C or ANYmal-D robot, chosen at random + +.. image:: ../_static/demos/multi_asset.jpg + :width: 100% + :alt: result of multi_asset.py + + +Rigid Object Collections +------------------------ + +Multiple rigid objects can be spawned in each environment and accessed/modified with a unified ``(env_ids, obj_ids)`` API. +While the user could also create multiple rigid objects by spawning them individually, the API is more user-friendly and +more efficient since it uses a single physics view under the hood to handle all the objects. + +.. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :lines: 135-179 + :dedent: + +The configuration :class:`~assets.RigidObjectCollectionCfg` is used to create the collection. It's attribute :attr:`~assets.RigidObjectCollectionCfg.rigid_objects` +is a dictionary containing :class:`~assets.RigidObjectCfg` objects. The keys serve as unique identifiers for each +rigid object in the collection. + + +Spawning different assets under the same prim path +-------------------------------------------------- + +It is possible to spawn different assets and USDs under the same prim path 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: 107-133 + :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: 182-215 + :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: 280-283 + :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/migration/migrating_from_isaacgymenvs.rst b/docs/source/migration/migrating_from_isaacgymenvs.rst index c903d83c3d..2632321917 100644 --- a/docs/source/migration/migrating_from_isaacgymenvs.rst +++ b/docs/source/migration/migrating_from_isaacgymenvs.rst @@ -45,9 +45,9 @@ Below is an example skeleton of a task config class: # env decimation = 2 episode_length_s = 5.0 - num_actions = 1 - num_observations = 4 - num_states = 0 + action_space = 1 + observation_space = 4 + state_space = 0 # task-specific parameters ... @@ -135,9 +135,9 @@ The following parameters must be set for each environment config: decimation = 2 episode_length_s = 5.0 - num_actions = 1 - num_observations = 4 - num_states = 0 + action_space = 1 + observation_space = 4 + state_space = 0 Note that the maximum episode length parameter (now ``episode_length_s``) is in seconds instead of steps as it was in IsaacGymEnvs. To convert between step count to seconds, use the equation: @@ -196,7 +196,7 @@ adding any other optional objects into the scene, such as lights. | self.sim = super().create_sim(self.device_id, self.graphics_device_id, | # clone, filter, and replicate | | self.physics_engine, self.sim_params) | self.scene.clone_environments(copy_from_source=False) | | self._create_ground_plane() | self.scene.filter_collisions(global_prim_paths=[]) | -| self._create_envs(self.num_envs, self.cfg["env"]['envSpacing'], | # add articultion to scene | +| self._create_envs(self.num_envs, self.cfg["env"]['envSpacing'], | # add articulation to scene | | int(np.sqrt(self.num_envs))) | self.scene.articulations["cartpole"] = self.cartpole | | | # add lights | | | light_cfg = sim_utils.DomeLightCfg(intensity=2000.0) | @@ -569,9 +569,9 @@ Task Config | | decimation = 2 | | asset: | episode_length_s = 5.0 | | assetRoot: "../../assets" | action_scale = 100.0 # [N] | -| assetFileName: "urdf/cartpole.urdf" | num_actions = 1 | -| | num_observations = 4 | -| enableCameraSensors: False | num_states = 0 | +| assetFileName: "urdf/cartpole.urdf" | action_space = 1 | +| | observation_space = 4 | +| enableCameraSensors: False | state_space = 0 | | | # reset | | sim: | max_cart_pos = 3.0 | | dt: 0.0166 # 1/60 s | initial_pole_angle_range = [-0.25, 0.25] | @@ -660,7 +660,7 @@ the need to set simulation parameters for actors in the task implementation. | self._create_ground_plane() | copy_from_source=False) | | self._create_envs(self.num_envs, | self.scene.filter_collisions( | | self.cfg["env"]['envSpacing'], | global_prim_paths=[]) | -| int(np.sqrt(self.num_envs))) | # add articultion to scene | +| int(np.sqrt(self.num_envs))) | # add articulation to scene | | | self.scene.articulations["cartpole"] = self.cartpole | | def _create_ground_plane(self): | # add lights | | plane_params = gymapi.PlaneParams() | light_cfg = sim_utils.DomeLightCfg( | diff --git a/docs/source/migration/migrating_from_omniisaacgymenvs.rst b/docs/source/migration/migrating_from_omniisaacgymenvs.rst index 50f9d5b9d6..7945ccb0b1 100644 --- a/docs/source/migration/migrating_from_omniisaacgymenvs.rst +++ b/docs/source/migration/migrating_from_omniisaacgymenvs.rst @@ -46,9 +46,9 @@ Below is an example skeleton of a task config class: # env decimation = 2 episode_length_s = 5.0 - num_actions = 1 - num_observations = 4 - num_states = 0 + action_space = 1 + observation_space = 4 + state_space = 0 # task-specific parameters ... @@ -158,9 +158,9 @@ The following parameters must be set for each environment config: decimation = 2 episode_length_s = 5.0 - num_actions = 1 - num_observations = 4 - num_states = 0 + action_space = 1 + observation_space = 4 + state_space = 0 RL Config Setup @@ -219,7 +219,7 @@ will automatically be created for the actor. This avoids the need to separately | self._cartpoles = ArticulationView( | # clone, filter, and replicate | | prim_paths_expr="/World/envs/.*/Cartpole", | self.scene.clone_environments(copy_from_source=False) | | name="cartpole_view", reset_xform_properties=False | self.scene.filter_collisions(global_prim_paths=[]) | -| ) | # add articultion to scene | +| ) | # add articulation to scene | | scene.add(self._cartpoles) | self.scene.articulations["cartpole"] = self.cartpole | | | # add lights | | | light_cfg = sim_utils.DomeLightCfg(intensity=2000.0) | @@ -501,9 +501,9 @@ Task config in Isaac Lab can be split into the main task configuration class and | clipObservations: 5.0 | decimation = 2 | | clipActions: 1.0 | episode_length_s = 5.0 | | controlFrequencyInv: 2 # 60 Hz | action_scale = 100.0 # [N] | -| | num_actions = 1 | -| sim: | num_observations = 4 | -| | num_states = 0 | +| | action_space = 1 | +| sim: | observation_space = 4 | +| | state_space = 0 | | dt: 0.0083 # 1/120 s | # reset | | use_gpu_pipeline: ${eq:${...pipeline},"gpu"} | max_cart_pos = 3.0 | | gravity: [0.0, 0.0, -9.81] | initial_pole_angle_range = [-0.25, 0.25] | @@ -638,7 +638,7 @@ Adding actors to the scene has been replaced by ``self.scene.articulations["cart | reset_xform_properties=False | copy_from_source=False) | | ) | self.scene.filter_collisions( | | scene.add(self._cartpoles) | global_prim_paths=[]) | -| return | # add articultion to scene | +| return | # add articulation to scene | | | self.scene.articulations["cartpole"] = self.cartpole | | def get_cartpole(self): | | | cartpole = Cartpole( | # add lights | diff --git a/docs/source/overview/core-concepts/actuators.rst b/docs/source/overview/core-concepts/actuators.rst index 7513709b49..15bde0cdb8 100644 --- a/docs/source/overview/core-concepts/actuators.rst +++ b/docs/source/overview/core-concepts/actuators.rst @@ -74,4 +74,4 @@ The following figure shows the actuator groups for a legged mobile manipulator: .. seealso:: We provide implementations for various explicit actuator models. These are detailed in - `omni.isaac.lab.actuators <../api/lab/omni.isaac.lab.actuators.html>`_ sub-package. + `omni.isaac.lab.actuators <../../api/lab/omni.isaac.lab.actuators.html>`_ sub-package. diff --git a/docs/source/overview/core-concepts/task_workflows.rst b/docs/source/overview/core-concepts/task_workflows.rst index 4f3c96e0f4..7aeb78e8dc 100644 --- a/docs/source/overview/core-concepts/task_workflows.rst +++ b/docs/source/overview/core-concepts/task_workflows.rst @@ -43,6 +43,16 @@ For example, the observation manager is responsible for computing the observatio computing the rewards, and the termination manager is responsible for computing the termination signal. This approach is known as the manager-based environment design in the framework. +.. image:: ../../_static/task-workflows/manager-based-light.svg + :class: only-light + :align: center + :alt: Manager-based Task Workflow + +.. image:: ../../_static/task-workflows/manager-based-dark.svg + :class: only-dark + :align: center + :alt: Manager-based Task Workflow + Manager-based environments promote modular implementations of tasks by decomposing the task into individual components that are managed by separate classes. Each component of the task, such as rewards, observations, termination can all be specified as individual configuration classes that are then passed to the corresponding @@ -94,6 +104,16 @@ of the environment. This approach does not require the manager classes. Instead, to implement their task through the APIs from the base classes :class:`envs.DirectRLEnv` or :class:`envs.DirectMARLEnv`. For users migrating from the `IsaacGymEnvs`_ and `OmniIsaacGymEnvs`_ framework, this workflow may be more familiar. +.. image:: ../../_static/task-workflows/direct-based-light.svg + :class: only-light + :align: center + :alt: Direct-based Task Workflow + +.. image:: ../../_static/task-workflows/direct-based-dark.svg + :class: only-dark + :align: center + :alt: Direct-based Task Workflow + When defining an environment with the direct-style implementation, we expect the user define a single class that implements the entire environment. The task class should inherit from the base classes :class:`envs.DirectRLEnv` or :class:`envs.DirectMARLEnv` and should have its corresponding configuration class that inherits from diff --git a/docs/source/overview/developer-guide/template.rst b/docs/source/overview/developer-guide/template.rst index defaa2b78c..e6c55b9b61 100644 --- a/docs/source/overview/developer-guide/template.rst +++ b/docs/source/overview/developer-guide/template.rst @@ -22,4 +22,36 @@ This template serves three distinct use cases: features and improvements in Isaac Lab. -To get started, please follow the instructions in the `extension template repository `_. +Installation +------------ + +Install Isaac Lab by following the `installation guide <../../setup/installation/index.html>`_. We recommend using the conda installation as it simplifies calling Python scripts from the terminal. + +Clone the extension template repository separately from the Isaac Lab installation (i.e. outside the IsaacLab directory): + +.. code:: bash + + # Option 1: HTTPS + git clone https://github.com/isaac-sim/IsaacLabExtensionTemplate.git + + # Option 2: SSH + git clone git@github.com:isaac-sim/IsaacLabExtensionTemplate.git + +Throughout the repository, the name ``ext_template`` only serves as an example and we provide a script to rename all the references to it automatically: + +.. code:: bash + + # Enter the repository + cd IsaacLabExtensionTemplate + + # Rename all occurrences of ext_template (in files/directories) to your_fancy_extension_name + python scripts/rename_template.py your_fancy_extension_name + +Using a python interpreter that has Isaac Lab installed, install the library: + +.. code:: bash + + python -m pip install -e exts/ext_template + + +For more details, please follow the instructions in the `extension template repository `_. diff --git a/docs/source/overview/environments.rst b/docs/source/overview/environments.rst index f42f3c34a5..5d08c22b6e 100644 --- a/docs/source/overview/environments.rst +++ b/docs/source/overview/environments.rst @@ -61,6 +61,10 @@ Classic environments that are based on IsaacGymEnvs implementation of MuJoCo-sty | | | | | | |cartpole-depth-direct-link|| | +------------------+-----------------------------+-------------------------------------------------------------------------+ + | |cartpole| | |cartpole-resnet-link| | Move the cart to keep the pole upwards in the classic cartpole control | + | | | based off of features extracted from perceptive inputs with pre-trained | + | | |cartpole-theia-link| | frozen vision encoders | + +------------------+-----------------------------+-------------------------------------------------------------------------+ .. |humanoid| image:: ../_static/tasks/classic/humanoid.jpg .. |ant| image:: ../_static/tasks/classic/ant.jpg @@ -69,8 +73,11 @@ 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-Camera-v0 `__ -.. |cartpole-depth-link| replace:: `Isaac-Cartpole-Depth-Camera-v0 `__ +.. |cartpole-rgb-link| replace:: `Isaac-Cartpole-RGB-v0 `__ +.. |cartpole-depth-link| replace:: `Isaac-Cartpole-Depth-v0 `__ +.. |cartpole-resnet-link| replace:: `Isaac-Cartpole-RGB-ResNet18-v0 `__ +.. |cartpole-theia-link| replace:: `Isaac-Cartpole-RGB-TheiaTiny-v0 `__ + .. |humanoid-direct-link| replace:: `Isaac-Humanoid-Direct-v0 `__ .. |ant-direct-link| replace:: `Isaac-Ant-Direct-v0 `__ @@ -102,6 +109,8 @@ for the reach environment: +--------------------+-------------------------+-----------------------------------------------------------------------------+ | |lift-cube| | |lift-cube-link| | Pick a cube and bring it to a sampled target position with the Franka robot | +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | |stack-cube| | |stack-cube-link| | Stack three cubes (bottom to top: blue, red, green) with the Franka robot | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ | |cabi-franka| | |cabi-franka-link| | Grasp the handle of a cabinet's drawer and open it with the Franka robot | | | | | | | |franka-direct-link| | | @@ -125,6 +134,7 @@ for the reach environment: .. |cabi-franka| image:: ../_static/tasks/manipulation/franka_open_drawer.jpg .. |cube-allegro| image:: ../_static/tasks/manipulation/allegro_cube.jpg .. |cube-shadow| image:: ../_static/tasks/manipulation/shadow_cube.jpg +.. |stack-franka| image:: ../_static/tasks/manipulation/franka_stack.jpg .. |reach-franka-link| replace:: `Isaac-Reach-Franka-v0 `__ .. |reach-ur10-link| replace:: `Isaac-Reach-UR10-v0 `__ @@ -135,6 +145,7 @@ for the reach environment: .. |franka-direct-link| replace:: `Isaac-Franka-Cabinet-Direct-v0 `__ .. |cube-allegro-link| replace:: `Isaac-Repose-Cube-Allegro-v0 `__ .. |allegro-direct-link| replace:: `Isaac-Repose-Cube-Allegro-Direct-v0 `__ +.. |stack-cube-link| replace:: `Isaac-Stack-Cube-Franka-v0 `__ .. |cube-shadow-link| replace:: `Isaac-Repose-Cube-Shadow-Direct-v0 `__ .. |cube-shadow-ff-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-FF-Direct-v0 `__ diff --git a/docs/source/overview/reinforcement-learning/rl_existing_scripts.rst b/docs/source/overview/reinforcement-learning/rl_existing_scripts.rst index 6cc88137f2..6ee865fd36 100644 --- a/docs/source/overview/reinforcement-learning/rl_existing_scripts.rst +++ b/docs/source/overview/reinforcement-learning/rl_existing_scripts.rst @@ -4,12 +4,12 @@ Reinforcement Learning Wrappers We provide wrappers to different reinforcement libraries. These wrappers convert the data from the environments into the respective libraries function argument and return types. -Stable-Baselines3 ------------------ + +RL-Games +-------- - Training an agent with - `Stable-Baselines3 `__ - on ``Isaac-Cartpole-v0``: + `RL-Games `__ on ``Isaac-Ant-v0``: .. tab-set:: :sync-group: os @@ -19,30 +19,65 @@ Stable-Baselines3 .. code:: bash - # install python module (for stable-baselines3) - ./isaaclab.sh -i sb3 + # install python module (for rl-games) + ./isaaclab.sh -i rl_games # run script for training - # note: we set the device to cpu since SB3 doesn't optimize for GPU anyway - ./isaaclab.sh -p source/standalone/workflows/sb3/train.py --task Isaac-Cartpole-v0 --headless --device cpu + ./isaaclab.sh -p source/standalone/workflows/rl_games/train.py --task Isaac-Ant-v0 --headless # run script for playing with 32 environments - ./isaaclab.sh -p source/standalone/workflows/sb3/play.py --task Isaac-Cartpole-v0 --num_envs 32 --checkpoint /PATH/TO/model.zip + ./isaaclab.sh -p source/standalone/workflows/rl_games/play.py --task Isaac-Ant-v0 --num_envs 32 --checkpoint /PATH/TO/model.pth # run script for recording video of a trained agent (requires installing `ffmpeg`) - ./isaaclab.sh -p source/standalone/workflows/sb3/play.py --task Isaac-Cartpole-v0 --headless --video --video_length 200 + ./isaaclab.sh -p source/standalone/workflows/rl_games/play.py --task Isaac-Ant-v0 --headless --video --video_length 200 .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows .. code:: batch - :: install python module (for stable-baselines3) - isaaclab.bat -i sb3 + :: install python module (for rl-games) + isaaclab.bat -i rl_games :: run script for training - :: note: we set the device to cpu since SB3 doesn't optimize for GPU anyway - isaaclab.bat -p source\standalone\workflows\sb3\train.py --task Isaac-Cartpole-v0 --headless --device cpu + isaaclab.bat -p source\standalone\workflows\rl_games\train.py --task Isaac-Ant-v0 --headless :: run script for playing with 32 environments - isaaclab.bat -p source\standalone\workflows\sb3\play.py --task Isaac-Cartpole-v0 --num_envs 32 --checkpoint /PATH/TO/model.zip + isaaclab.bat -p source\standalone\workflows\rl_games\play.py --task Isaac-Ant-v0 --num_envs 32 --checkpoint /PATH/TO/model.pth :: run script for recording video of a trained agent (requires installing `ffmpeg`) - isaaclab.bat -p source\standalone\workflows\sb3\play.py --task Isaac-Cartpole-v0 --headless --video --video_length 200 + isaaclab.bat -p source\standalone\workflows\rl_games\play.py --task Isaac-Ant-v0 --headless --video --video_length 200 + +RSL-RL +------ + +- Training an agent with + `RSL-RL `__ on ``Isaac-Reach-Franka-v0``: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # install python module (for rsl-rl) + ./isaaclab.sh -i rsl_rl + # run script for training + ./isaaclab.sh -p source/standalone/workflows/rsl_rl/train.py --task Isaac-Reach-Franka-v0 --headless + # run script for playing with 32 environments + ./isaaclab.sh -p source/standalone/workflows/rsl_rl/play.py --task Isaac-Reach-Franka-v0 --num_envs 32 --load_run run_folder_name --checkpoint model.pt + # run script for recording video of a trained agent (requires installing `ffmpeg`) + ./isaaclab.sh -p source/standalone/workflows/rsl_rl/play.py --task Isaac-Reach-Franka-v0 --headless --video --video_length 200 + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: install python module (for rsl-rl) + isaaclab.bat -i rsl_rl + :: run script for training + isaaclab.bat -p source\standalone\workflows\rsl_rl\train.py --task Isaac-Reach-Franka-v0 --headless + :: run script for playing with 32 environments + isaaclab.bat -p source\standalone\workflows\rsl_rl\play.py --task Isaac-Reach-Franka-v0 --num_envs 32 --load_run run_folder_name --checkpoint model.pt + :: run script for recording video of a trained agent (requires installing `ffmpeg`) + isaaclab.bat -p source\standalone\workflows\rsl_rl\play.py --task Isaac-Reach-Franka-v0 --headless --video --video_length 200 SKRL ---- @@ -129,48 +164,12 @@ SKRL :: run script for playing with 32 environments with the MAPPO algorithm (IPPO is also supported) isaaclab.bat -p source\standalone\workflows\skrl\play.py --task Isaac-Shadow-Hand-Over-Direct-v0 --num_envs 32 --algorithm MAPPO --checkpoint /PATH/TO/model.pt -RL-Games --------- - -- Training an agent with - `RL-Games `__ on ``Isaac-Ant-v0``: - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # install python module (for rl-games) - ./isaaclab.sh -i rl_games - # run script for training - ./isaaclab.sh -p source/standalone/workflows/rl_games/train.py --task Isaac-Ant-v0 --headless - # run script for playing with 32 environments - ./isaaclab.sh -p source/standalone/workflows/rl_games/play.py --task Isaac-Ant-v0 --num_envs 32 --checkpoint /PATH/TO/model.pth - # run script for recording video of a trained agent (requires installing `ffmpeg`) - ./isaaclab.sh -p source/standalone/workflows/rl_games/play.py --task Isaac-Ant-v0 --headless --video --video_length 200 - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: install python module (for rl-games) - isaaclab.bat -i rl_games - :: run script for training - isaaclab.bat -p source\standalone\workflows\rl_games\train.py --task Isaac-Ant-v0 --headless - :: run script for playing with 32 environments - isaaclab.bat -p source\standalone\workflows\rl_games\play.py --task Isaac-Ant-v0 --num_envs 32 --checkpoint /PATH/TO/model.pth - :: run script for recording video of a trained agent (requires installing `ffmpeg`) - isaaclab.bat -p source\standalone\workflows\rl_games\play.py --task Isaac-Ant-v0 --headless --video --video_length 200 - -RSL-RL ------- +Stable-Baselines3 +----------------- - Training an agent with - `RSL-RL `__ on ``Isaac-Reach-Franka-v0``: + `Stable-Baselines3 `__ + on ``Isaac-Cartpole-v0``: .. tab-set:: :sync-group: os @@ -180,28 +179,30 @@ RSL-RL .. code:: bash - # install python module (for rsl-rl) - ./isaaclab.sh -i rsl_rl + # install python module (for stable-baselines3) + ./isaaclab.sh -i sb3 # run script for training - ./isaaclab.sh -p source/standalone/workflows/rsl_rl/train.py --task Isaac-Reach-Franka-v0 --headless + # note: we set the device to cpu since SB3 doesn't optimize for GPU anyway + ./isaaclab.sh -p source/standalone/workflows/sb3/train.py --task Isaac-Cartpole-v0 --headless --device cpu # run script for playing with 32 environments - ./isaaclab.sh -p source/standalone/workflows/rsl_rl/play.py --task Isaac-Reach-Franka-v0 --num_envs 32 --load_run run_folder_name --checkpoint model.pt + ./isaaclab.sh -p source/standalone/workflows/sb3/play.py --task Isaac-Cartpole-v0 --num_envs 32 --checkpoint /PATH/TO/model.zip # run script for recording video of a trained agent (requires installing `ffmpeg`) - ./isaaclab.sh -p source/standalone/workflows/rsl_rl/play.py --task Isaac-Reach-Franka-v0 --headless --video --video_length 200 + ./isaaclab.sh -p source/standalone/workflows/sb3/play.py --task Isaac-Cartpole-v0 --headless --video --video_length 200 .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows .. code:: batch - :: install python module (for rsl-rl) - isaaclab.bat -i rsl_rl + :: install python module (for stable-baselines3) + isaaclab.bat -i sb3 :: run script for training - isaaclab.bat -p source\standalone\workflows\rsl_rl\train.py --task Isaac-Reach-Franka-v0 --headless + :: note: we set the device to cpu since SB3 doesn't optimize for GPU anyway + isaaclab.bat -p source\standalone\workflows\sb3\train.py --task Isaac-Cartpole-v0 --headless --device cpu :: run script for playing with 32 environments - isaaclab.bat -p source\standalone\workflows\rsl_rl\play.py --task Isaac-Reach-Franka-v0 --num_envs 32 --load_run run_folder_name --checkpoint model.pt + isaaclab.bat -p source\standalone\workflows\sb3\play.py --task Isaac-Cartpole-v0 --num_envs 32 --checkpoint /PATH/TO/model.zip :: run script for recording video of a trained agent (requires installing `ffmpeg`) - isaaclab.bat -p source\standalone\workflows\rsl_rl\play.py --task Isaac-Reach-Franka-v0 --headless --video --video_length 200 + isaaclab.bat -p source\standalone\workflows\sb3\play.py --task Isaac-Cartpole-v0 --headless --video --video_length 200 All the scripts above log the training progress to `Tensorboard`_ in the ``logs`` directory in the root of the repository. The logs directory follows the pattern ``logs///``, where ```` diff --git a/docs/source/features/tiled_rendering.rst b/docs/source/overview/sensors/camera.rst similarity index 52% rename from docs/source/features/tiled_rendering.rst rename to docs/source/overview/sensors/camera.rst index 55d22561a4..0ef72196c5 100644 --- a/docs/source/features/tiled_rendering.rst +++ b/docs/source/overview/sensors/camera.rst @@ -1,28 +1,26 @@ -Tiled-Camera Rendering -====================== +.. _overview_sensors_camera: -.. currentmodule:: omni.isaac.lab + +Camera +======== + +Camera sensors are uniquely defined by the use of the ``render_product``, a structure for managing data generated by the rendering pipeline (images). Isaac Lab provides the ability to fully control how these renderings are created through camera parameters like focal length, pose, type, etc... and what kind of data you want to render through the use of Annotators, allowing you to record not only RGB, but also Instance segmentation, object pose, object ID, etc... + +Rendered images are unique among the supported data types in Isaac Lab due to the inherently large bandwidth requirements for moving those data. A single 800 x 600 image with 32-bit color (a single float per pixel) clocks in at just under 2 MB. If we render at 60 fps and record every frame, that camera needs to move 120 MB/s. Multiply this by the number of cameras in an environment and environments in a simulation, and you can quickly see how scaling a naive vectorization of camera data could lead to bandwidth challenges. NVIDIA's Isaac Lab leverages our expertise in GPU hardware to provide an API that specifically addresses these scaling challenges in the rendering pipeline. + +Tiled Rendering +~~~~~~~~~~~~~~~~~ .. note:: This feature is only available from Isaac Sim version 4.2.0 onwards. Tiled rendering in combination with image processing networks require heavy memory resources, especially - at larger resolutions. We recommend running at 512 cameras in the scene on RTX 4090 GPUs or similar. - + at larger resolutions. We recommend running 512 cameras in the scene on RTX 4090 GPUs or similar. -Tiled rendering APIs provide a vectorized interface for collecting data from camera sensors. -This is useful for reinforcement learning environments requiring vision in the loop. -Tiled rendering works by concatenating camera outputs from multiple cameras and rendering -one single large image instead of multiple smaller images that would have been produced -by each individual camera. This reduces the amount of time required for rendering and -provides a more efficient API for working with vision data. +The Tiled Rendering APIs provide a vectorized interface for collecting data from camera sensors. This is useful for reinforcement learning environments where parallelization can be exploited to accelerate data collection and thus the training loop. Tiled rendering works by using a single ``render_product`` for **all** clones of a single camera in the scene. The desired dimensions of a single image and the number of environments are used to compute a much larger ``render_product``, consisting of the tiled individual renders from the separate clones of the camera. When all cameras have populated their buffers the render product is "completed" and can be moved around as a single, large image, dramatically reducing the overhead for moving the data from the host to the device, for example. Only a single call is used to synchronize the device data, instead of one call per camera, and this is a big part of what makes the Tiled Rendering API more efficient for working with vision data. -Isaac Lab provides tiled rendering APIs for RGB, depth, along with other annotators through the :class:`~sensors.TiledCamera` -class. Configurations for the tiled rendering APIs can be defined through the :class:`~sensors.TiledCameraCfg` -class, specifying parameters such as the regex expression for all camera paths, the transform -for the cameras, the desired data type, the type of cameras to add to the scene, and the camera -resolution. +Isaac Lab provides tiled rendering APIs for RGB, depth, along with other annotators through the :class:`~sensors.TiledCamera` class. Configurations for the tiled rendering APIs can be defined through the :class:`~sensors.TiledCameraCfg` class, specifying parameters such as the regex expression for all camera paths, the transform for the cameras, the desired data type, the type of cameras to add to the scene, and the camera resolution. .. code-block:: python @@ -37,8 +35,7 @@ resolution. height=80, ) -To access the tiled rendering interface, a :class:`~sensors.TiledCamera` object can be created and used -to retrieve data from the cameras. +To access the tiled rendering interface, a :class:`~sensors.TiledCamera` object can be created and used to retrieve data from the cameras. .. code-block:: python @@ -46,19 +43,17 @@ to retrieve data from the cameras. data_type = "rgb" data = tiled_camera.data.output[data_type] -The returned data will be transformed into the shape (num_cameras, height, width, num_channels), which -can be used directly as observation for reinforcement learning. +The returned data will be transformed into the shape (num_cameras, height, width, num_channels), which can be used directly as observation for reinforcement learning. -When working with rendering, make sure to add the ``--enable_cameras`` argument when launching the -environment. For example: +When working with rendering, make sure to add the ``--enable_cameras`` argument when launching the environment. For example: .. code-block:: shell python source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-RGB-Camera-Direct-v0 --headless --enable_cameras -Annotators and Data Types -------------------------- +Annotators +~~~~~~~~~~~~~~~~~ Both :class:`~sensors.TiledCamera` and :class:`~sensors.Camera` classes provide APIs for retrieving various types annotator data from replicator: @@ -76,6 +71,11 @@ Both :class:`~sensors.TiledCamera` and :class:`~sensors.Camera` classes provide RGB and RGBA ~~~~~~~~~~~~ +.. figure:: ../../_static/overview/overview_sensors_rgb.png + :align: center + :figwidth: 100% + :alt: A scene captured in RGB + ``rgb`` data type returns a 3-channel RGB colored image of type ``torch.uint8``, with dimension (B, H, W, 3). ``rgba`` data type returns a 4-channel RGBA colored image of type ``torch.uint8``, with dimension (B, H, W, 4). @@ -85,6 +85,11 @@ To convert the ``torch.uint8`` data to ``torch.float32``, divide the buffer by 2 Depth and Distances ~~~~~~~~~~~~~~~~~~~ +.. figure:: ../../_static/overview/overview_sensors_depth.png + :align: center + :figwidth: 100% + :alt: A scene captured in RGB + ``distance_to_camera`` returns a single-channel depth image with distance to the camera optical center. The dimension for this annotator is (B, H, W, 1) and has type ``torch.float32``. ``distance_to_image_plane`` returns a single-channel depth image with distances of 3D points from the camera plane along the camera's Z-axis. The dimension for this annotator is (B, H, W, 1) and has type ``torch.float32``. @@ -94,6 +99,11 @@ Depth and Distances Normals ~~~~~~~ +.. figure:: ../../_static/overview/overview_sensors_normals.png + :align: center + :figwidth: 100% + :alt: A scene captured in RGB + ``normals`` returns an image containing the local surface normal vectors at each pixel. The buffer has dimension (B, H, W, 3), containing the (x, y, z) information for each vector, and has data type ``torch.float32``. Motion Vectors @@ -104,6 +114,11 @@ Motion Vectors Semantic Segmentation ~~~~~~~~~~~~~~~~~~~~~ +.. figure:: ../../_static/overview/overview_sensors_semantic.png + :align: center + :figwidth: 100% + :alt: A scene captured in RGB + ``semantic_segmentation`` outputs semantic segmentation of each entity in the camera’s viewport that has semantic labels. In addition to the image buffer, an ``info`` dictionary can be retrieved with ``tiled_camera.data.info['semantic_segmentation']`` containing ID to labels information. - If ``colorize_semantic_segmentation=True`` in the camera config, a 4-channel RGBA image will be returned with dimension (B, H, W, 4) and type ``torch.uint8``. The info ``idToLabels`` dictionary will be the mapping from color to semantic labels. @@ -113,6 +128,11 @@ Semantic Segmentation Instance ID Segmentation ~~~~~~~~~~~~~~~~~~~~~~~~ +.. figure:: ../../_static/overview/overview_sensors_instanceID.png + :align: center + :figwidth: 100% + :alt: A scene captured in RGB + ``instance_id_segmentation_fast`` outputs instance ID segmentation of each entity in the camera’s viewport. The instance ID is unique for each prim in the scene with different paths. In addition to the image buffer, an ``info`` dictionary can be retrieved with ``tiled_camera.data.info['instance_id_segmentation_fast']`` containing ID to labels information. The main difference between ``instance_id_segmentation_fast`` and ``instance_segmentation_fast`` are that instance segmentation annotator goes down the hierarchy to the lowest level prim which has semantic labels, where instance ID segmentation always goes down to the leaf prim. @@ -124,8 +144,51 @@ The main difference between ``instance_id_segmentation_fast`` and ``instance_seg Instance Segmentation """"""""""""""""""""" +.. figure:: ../../_static/overview/overview_sensors_instance.png + :align: center + :figwidth: 100% + :alt: A scene captured in RGB + ``instance_segmentation_fast`` outputs instance segmentation of each entity in the camera’s viewport. In addition to the image buffer, an ``info`` dictionary can be retrieved with ``tiled_camera.data.info['instance_segmentation_fast']`` containing ID to labels and ID to semantic information. -- If ``colorize_instance_segmentation=True`` in the camera config, a 4-channel RGBA image will be returned with dimension (B, H, W, 4) and type ``torch.uint8``. The info ``idToLabels`` dictionary will be the mapping from color to USD prim path of that semantic entity. The info ``idToSemantics`` dictionary will be the mapping from color to semantic labels of that semantic entity. +- If ``colorize_instance_segmentation=True`` in the camera config, a 4-channel RGBA image will be returned with dimension (B, H, W, 4) and type ``torch.uint8``. + +- If ``colorize_instance_segmentation=False``, a buffer of dimension (B, H, W, 1) of type ``torch.int32`` will be returned, containing the instance ID of each pixel. + +The info ``idToLabels`` dictionary will be the mapping from color to USD prim path of that semantic entity. The info ``idToSemantics`` dictionary will be the mapping from color to semantic labels of that semantic entity. + + +Current Limitations +------------------- -- If ``colorize_instance_segmentation=False``, a buffer of dimension (B, H, W, 1) of type ``torch.int32`` will be returned, containing the instance ID of each pixel. The info ``idToLabels`` dictionary will be the mapping from instance ID to USD prim path of that semantic entity. The info ``idToSemantics`` dictionary will be the mapping from instance ID to semantic labels of that semantic entity. +Due to current limitations in the renderer, we can have only **one** :class:`~sensors.TiledCamera` instance in the scene. +For use cases that require a setup with more than one camera, we can imitate the multi-camera behavior by moving the location +of the camera in between render calls in a step. + +For example, in a stereo vision setup, the below snippet can be implemented: + +.. code-block:: python + + # render image from "first" camera + camera_data_1 = self._tiled_camera.data.output["rgb"].clone() / 255.0 + # update camera transform to the "second" camera location + self._tiled_camera.set_world_poses( + positions=pos, + orientations=rot, + convention="world" + ) + # step the renderer + self.sim.render() + self._tiled_camera.update(0, force_recompute=True) + # render image from "second" camera + camera_data_2 = self._tiled_camera.data.output["rgb"].clone() / 255.0 + +Note that this approach still limits the rendering resolution to be identical for all cameras. Currently, there is no workaround +to achieve different resolution images using :class:`~sensors.TiledCamera`. The best approach is to use the largest resolution out of all of the +desired resolutions and add additional scaling or cropping operations to the rendered output as a post-processing step. + +In addition, there may be visible quality differences when comparing render outputs of different numbers of environments. +Currently, any combined resolution that has a width less than 265 pixels or height less than 265 will automatically switch +to the DLAA anti-aliasing mode, which does not perform up-sampling during anti-aliasing. For resolutions larger than 265 in both +width and height dimensions, we default to using the "performance" DLSS mode for anti-aliasing for performance benefits. +Anti-aliasing modes and other rendering parameters can be specified in the :class:`~sim.RenderCfg`. diff --git a/docs/source/overview/sensors/contact_sensor.rst b/docs/source/overview/sensors/contact_sensor.rst new file mode 100644 index 0000000000..72239fd287 --- /dev/null +++ b/docs/source/overview/sensors/contact_sensor.rst @@ -0,0 +1,145 @@ +.. _overview_sensors_contact: + +Contact Sensor +================ + +.. figure:: ../../_static/overview/overview_sensors_contact_diagram.png + :align: center + :figwidth: 100% + :alt: A contact sensor with filtering + +The contact sensor is designed to return the net contact force acting on a given ridgid body. The sensor is written to behave as a physical object, and so the "scope" of the contact sensor is limited to the body (or bodies) that defines it. There are multiple ways to define this scope, depending on your need to filter the forces coming from the contact. + +By default, the reported force is the total contact force, but your application may only care about contact forces due to specific objects. Retrieving contact forces from specific objects requires filtering, and this can only be done in a "many-to-one" way. A multi-legged robot that needs filterable contact information for its feet would require one sensor per foot to be defined in the environment, but a robotic hand with contact sensors on the tips of each finger can be defined with a single sensor. + +Consider a simple environment with an Anymal Quadruped and a block + +.. code-block:: python + + @configclass + class ContactSensorsSceneCfg(InteractiveSceneCfg): + """Design the scene with sensors on the robot.""" + + # 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)) + ) + + # robot + robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # Rigid Object + cube = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube", + spawn=sim_utils.CuboidCfg( + size=(0.5,0.5,0.1), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=100.0), + collision_props=sim_utils.CollisionPropertiesCfg(), + physics_material=sim_utils.RigidBodyMaterialCfg(static_friction=1.0), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.5, 0.5, 0.05)), + ) + + contact_forces_LF = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/LF_FOOT", + update_period=0.0, + history_length=6, + debug_vis=True, + filter_prim_paths_expr=["{ENV_REGEX_NS}/Cube"], + ) + + contact_forces_RF = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/RF_FOOT", + update_period=0.0, + history_length=6, + debug_vis=True, + filter_prim_paths_expr=["{ENV_REGEX_NS}/Cube"], + ) + + contact_forces_H = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/.*H_FOOT", + update_period=0.0, + history_length=6, + debug_vis=True, + ) + +We define the sensors on the feet of the robot in two different ways. The front feet are independent sensors (one sensor body per foot) and the "Cube" is placed under the left foot. The hind feet are defined as a single sensor with multiple bodies. + +We can then run the scene and print the data from the sensors + +.. code-block:: python + + def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): + . + . + . + # Simulate physics + while simulation_app.is_running(): + . + . + . + # print information from the sensors + print("-------------------------------") + print(scene["contact_forces_LF"]) + print("Received force matrix of: ", scene["contact_forces_LF"].data.force_matrix_w) + print("Received contact force of: ", scene["contact_forces_LF"].data.net_forces_w) + print("-------------------------------") + print(scene["contact_forces_RF"]) + print("Received force matrix of: ", scene["contact_forces_RF"].data.force_matrix_w) + print("Received contact force of: ", scene["contact_forces_RF"].data.net_forces_w) + print("-------------------------------") + print(scene["contact_forces_H"]) + print("Received force matrix of: ", scene["contact_forces_H"].data.force_matrix_w) + print("Received contact force of: ", scene["contact_forces_H"].data.net_forces_w) + +Here, we print both the net contact force and the filtered force matrix for each contact sensor defined in the scene. The front left and front right feet report the following + +.. code-block:: bash + + ------------------------------- + Contact sensor @ '/World/envs/env_.*/Robot/LF_FOOT': + view type : + update period (s) : 0.0 + number of bodies : 1 + body names : ['LF_FOOT'] + + Received force matrix of: tensor([[[[-1.3923e-05, 1.5727e-04, 1.1032e+02]]]], device='cuda:0') + Received contact force of: tensor([[[-1.3923e-05, 1.5727e-04, 1.1032e+02]]], device='cuda:0') + ------------------------------- + Contact sensor @ '/World/envs/env_.*/Robot/RF_FOOT': + view type : + update period (s) : 0.0 + number of bodies : 1 + body names : ['RF_FOOT'] + + Received force matrix of: tensor([[[[0., 0., 0.]]]], device='cuda:0') + Received contact force of: tensor([[[1.3529e-05, 0.0000e+00, 1.0069e+02]]], device='cuda:0') + +Notice that even with filtering, both sensors report the net contact force acting on the foot. However only the left foot has a non zero "force matrix", because the right foot isn't standing on the filtered body, ``/World/envs/env_.*/Cube``. Now, checkout the data coming from the hind feet! + +.. code-block:: bash + + ------------------------------- + Contact sensor @ '/World/envs/env_.*/Robot/.*H_FOOT': + view type : + update period (s) : 0.0 + number of bodies : 2 + body names : ['LH_FOOT', 'RH_FOOT'] + + Received force matrix of: None + Received contact force of: tensor([[[9.7227e-06, 0.0000e+00, 7.2364e+01], + [2.4322e-05, 0.0000e+00, 1.8102e+02]]], device='cuda:0') + +In this case, the contact sensor has two bodies: the left and right hind feet. When the force matrix is queried, the result is ``None`` because this is a many body sensor, and presently Isaac Lab only supports "many to one" contact force filtering. Unlike the single body contact sensor, the reported force tensor has multiple entries, with each "row" corresponding to the contact force on a single body of the sensor (matching the ordering at construction). + +.. dropdown:: Code for contact_sensor.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/demos/sensors/contact_sensor.py + :language: python + :linenos: diff --git a/docs/source/overview/sensors/frame_transformer.rst b/docs/source/overview/sensors/frame_transformer.rst new file mode 100644 index 0000000000..d60dd9304c --- /dev/null +++ b/docs/source/overview/sensors/frame_transformer.rst @@ -0,0 +1,158 @@ +.. _overview_sensors_frame_transformer: + +Frame Transformer +==================== + +.. figure:: ../../_static/overview/overview_sensors_frame_transformer.png + :align: center + :figwidth: 100% + :alt: A diagram outlining the basic geometry of frame transformations + +.. + Do YOU want to know where things are relative to other things at a glance? Then the frame transformer is the sensor for you!* + +One of the most common operations that needs to be performed within a physics simulation is the frame transformation: rewriting a vector or quaternion in the basis of an arbitrary euclidean coordinate system. There are many ways to accomplish this within Isaac and USD, but these methods can be cumbersome to implement within Isaac Lab's GPU based simulation and cloned environments. To mitigate this problem, we have designed the Frame Transformer Sensor, that tracks and calculate the relative frame transformations for rigid bodies of interest to the scene. + +The sensory is minimally defined by a source frame and a list of target frames. These definitions take the form of a prim path (for the source) and list of regex capable prim paths the rigid bodies to be tracked (for the targets). + +.. code-block:: python + + @configclass + class FrameTransformerSensorSceneCfg(InteractiveSceneCfg): + """Design the scene with sensors on the robot.""" + + # 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)) + ) + + # robot + robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # Rigid Object + cube = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube", + spawn=sim_utils.CuboidCfg( + size=(1,1,1), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=100.0), + collision_props=sim_utils.CollisionPropertiesCfg(), + physics_material=sim_utils.RigidBodyMaterialCfg(static_friction=1.0), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(5, 0, 0.5)), + ) + + specific_transforms = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/base", + target_frames=[ + FrameTransformerCfg.FrameCfg(prim_path="{ENV_REGEX_NS}/Robot/LF_FOOT"), + FrameTransformerCfg.FrameCfg(prim_path="{ENV_REGEX_NS}/Robot/RF_FOOT"), + ], + debug_vis=True, + ) + + cube_transform = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/base", + target_frames=[ + FrameTransformerCfg.FrameCfg(prim_path="{ENV_REGEX_NS}/Cube") + ], + debug_vis=False, + ) + + robot_transforms = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/base", + target_frames=[ + FrameTransformerCfg.FrameCfg(prim_path="{ENV_REGEX_NS}/Robot/.*") + ], + debug_vis=False, + ) + + +We can now run the scene and query the sensor for data + +.. code-block:: python + + def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): + . + . + . + # Simulate physics + while simulation_app.is_running(): + . + . + . + + # print information from the sensors + print("-------------------------------") + print(scene["specific_transforms"]) + print("relative transforms:", scene["specific_transforms"].data.target_pos_source) + print("relative orientations:", scene["specific_transforms"].data.target_quat_source) + print("-------------------------------") + print(scene["cube_transform"]) + print("relative transform:", scene["cube_transform"].data.target_pos_source) + print("-------------------------------") + print(scene["robot_transforms"]) + print("relative transforms:", scene["robot_transforms"].data.target_pos_source) + +Let's take a look at the result for tracking specific objects. First, we can take a look at the data coming from the sensors on the feet + +.. code-block:: bash + + ------------------------------- + FrameTransformer @ '/World/envs/env_.*/Robot/base': + tracked body frames: ['base', 'LF_FOOT', 'RF_FOOT'] + number of envs: 1 + source body frame: base + target frames (count: ['LF_FOOT', 'RF_FOOT']): 2 + + relative transforms: tensor([[[ 0.4658, 0.3085, -0.4840], + [ 0.4487, -0.2959, -0.4828]]], device='cuda:0') + relative orientations: tensor([[[ 0.9623, 0.0072, -0.2717, -0.0020], + [ 0.9639, 0.0052, -0.2663, -0.0014]]], device='cuda:0') + +.. figure:: ../../_static/overview/overview_sensors_ft_visualizer.png + :align: center + :figwidth: 100% + :alt: The frame transformer visualizer + +By activating the visualizer, we can see that the frames of the feet are rotated "upward" slightly. We can also see the explicit relative positions and rotations by querying the sensor for data, which returns these values as a list with the same order as the tracked frames. This becomes even more apparent if we examine the transforms specified by regex. + +.. code-block:: bash + + ------------------------------- + FrameTransformer @ '/World/envs/env_.*/Robot/base': + tracked body frames: ['base', 'LF_FOOT', 'LF_HIP', 'LF_SHANK', 'LF_THIGH', 'LH_FOOT', 'LH_HIP', 'LH_SHANK', 'LH_THIGH', 'RF_FOOT', 'RF_HIP', 'RF_SHANK', 'RF_THIGH', 'RH_FOOT', 'RH_HIP', 'RH_SHANK', 'RH_THIGH', 'base'] + number of envs: 1 + source body frame: base + target frames (count: ['LF_FOOT', 'LF_HIP', 'LF_SHANK', 'LF_THIGH', 'LH_FOOT', 'LH_HIP', 'LH_SHANK', 'LH_THIGH', 'RF_FOOT', 'RF_HIP', 'RF_SHANK', 'RF_THIGH', 'RH_FOOT', 'RH_HIP', 'RH_SHANK', 'RH_THIGH', 'base']): 17 + + relative transforms: tensor([[[ 4.6581e-01, 3.0846e-01, -4.8398e-01], + [ 2.9990e-01, 1.0400e-01, -1.7062e-09], + [ 2.1409e-01, 2.9177e-01, -2.4214e-01], + [ 3.5980e-01, 1.8780e-01, 1.2608e-03], + [-4.8813e-01, 3.0973e-01, -4.5927e-01], + [-2.9990e-01, 1.0400e-01, 2.7044e-09], + [-2.1495e-01, 2.9264e-01, -2.4198e-01], + [-3.5980e-01, 1.8780e-01, 1.5582e-03], + [ 4.4871e-01, -2.9593e-01, -4.8277e-01], + [ 2.9990e-01, -1.0400e-01, -2.7057e-09], + [ 1.9971e-01, -2.8554e-01, -2.3778e-01], + [ 3.5980e-01, -1.8781e-01, -9.1049e-04], + [-5.0090e-01, -2.9095e-01, -4.5746e-01], + [-2.9990e-01, -1.0400e-01, 6.3592e-09], + [-2.1860e-01, -2.8251e-01, -2.5163e-01], + [-3.5980e-01, -1.8779e-01, -1.8792e-03], + [ 0.0000e+00, 0.0000e+00, 0.0000e+00]]], device='cuda:0') + +Here, the sensor is tracking all rigid body children of ``Robot/base``, but this expression is **inclusive**, meaning that the source body itself is also a target. This can be seen both by examining the source and target list, where ``base`` appears twice, and also in the returned data, where the sensor returns the relative transform to itself, (0, 0, 0). + +.. dropdown:: Code for frame_transformer_sensor.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/demos/sensors/frame_transformer_sensor.py + :language: python + :linenos: diff --git a/docs/source/overview/sensors/index.rst b/docs/source/overview/sensors/index.rst new file mode 100644 index 0000000000..496b4464a1 --- /dev/null +++ b/docs/source/overview/sensors/index.rst @@ -0,0 +1,20 @@ +.. _overview_sensors: + +Sensors +========= + +In this section, we will overview the various sensor APIs provided by Isaac Lab. + +Every sensor in Isaac Lab inherits from the ``SensorBase`` abstract class that provides the core functionality inherent to all sensors, which is to provide access to "measurements" of the scene. These measurements can take many forms such as ray-casting results, camera rendered images, or even simply ground truth data queried directly from the simulation (such as poses). Whatever the data may be, we can think of the sensor as having a buffer that is periodically updated with measurements by querying the scene. This ``update_period`` is defined in "simulated" seconds, meaning that even if the flow of time in the simulation is dilated relative to the real world, the sensor will update at the appropriate rate. The ``SensorBase`` is also designed with vectorizability in mind, holding the buffers for all copies of the sensor across cloned environments. + +Updating the buffers is done by overriding the ``_update_buffers_impl`` abstract method of the ``SensorBase`` class. On every time-step of the simulation, ``dt``, all sensors are queried for an update. During this query, the total time since the last update is incremented by ``dt`` for every buffer managed by that particular sensor. If the total time is greater than or equal to the ``update_period`` for a buffer, then that buffer is flagged to be updated on the next query. + +The following pages describe the available sensors in more detail: + +.. toctree:: + :maxdepth: 1 + + camera + contact_sensor + frame_transformer + ray_caster diff --git a/docs/source/overview/sensors/ray_caster.rst b/docs/source/overview/sensors/ray_caster.rst new file mode 100644 index 0000000000..b2846c9602 --- /dev/null +++ b/docs/source/overview/sensors/ray_caster.rst @@ -0,0 +1,108 @@ +.. _overview_sensors_ray_caster: + +Ray Caster +============= + +.. figure:: ../../_static/overview/overview_sensors_rc_patterns.png + :align: center + :figwidth: 100% + :alt: A diagram outlining the basic geometry of frame transformations + +The Ray Caster sensor (and the ray caster camera) are similar to RTX based rendering in that they both involve casting rays. The difference here is that the rays cast by the Ray Caster sensor return strictly collision information along the cast, and the direction of each individual ray can be specified. They do not bounce, nor are they affected by things like materials or opacity. For each ray specified by the sensor, a line is traced along the path of the ray and the location of first collision with the specified mesh is returned. This is the method used by some of our quadruped examples to measure the local height field. + +To keep the sensor performant when there are many cloned environments, the line tracing is done directly in `Warp `_. This is the reason why specific meshes need to be identified to cast against: that mesh data is loaded onto the device by warp when the sensor is initialized. As a consequence, the current iteration of this sensor only works for literally static meshes (meshes that *are not changed from the defaults specified in their USD file*). This constraint will be removed in future releases. + +Using a ray caster sensor requires a **pattern** and a parent xform to be attached to. The pattern defines how the rays are cast, while the prim properties defines the orientation and position of the sensor (additional offsets can be specified for more exact placement). Isaac Lab supports a number of ray casting pattern configurations, including a generic LIDAR and grid pattern. + +.. code-block:: python + + @configclass + class RaycasterSensorSceneCfg(InteractiveSceneCfg): + """Design the scene with sensors on the robot.""" + + # ground plane with texture for interesting casting results + ground = AssetBaseCfg( + prim_path="/World/Ground", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Environments/Terrains/rough_plane.usd", + scale = (1,1,1), + ) + ) + + # lights + dome_light = AssetBaseCfg( + prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + ) + + # robot + robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + ray_caster = RayCasterCfg( + prim_path="{ENV_REGEX_NS}/Robot/base/lidar_cage", + update_period = 1/60, + offset=RayCasterCfg.OffsetCfg(pos=(0,0,0.5)), + mesh_prim_paths=["/World/Ground"], + pattern_cfg=patterns.LidarPatternCfg( + channels = 100, + vertical_fov_range=[-90, 90], + horizontal_fov_range = [-90,90], + horizontal_res=1.0), + debug_vis=True, + ) + + +Notice that the units on the pattern config is in degrees! Also, we enable visualization here to explicitly show the pattern in the rendering, but this is not required and should be disabled for performance tuning. + +.. figure:: ../../_static/overview/overview_sensors_rc_visualizer.png + :align: center + :figwidth: 100% + :alt: Lidar Pattern visualized + +Querying the sensor for data can be done at simulation run time like any other sensor. + +.. code-block:: python + + def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): + . + . + . + # Simulate physics + while simulation_app.is_running(): + . + . + . + # print information from the sensors + print("-------------------------------") + print(scene["ray_caster"]) + print("Ray cast hit results: ", scene["ray_caster"].data.ray_hits_w) + + +.. code-block:: bash + + ------------------------------- + Ray-caster @ '/World/envs/env_.*/Robot/base/lidar_cage': + view type : + update period (s) : 0.016666666666666666 + number of meshes : 1 + number of sensors : 1 + number of rays/sensor: 18000 + total number of rays : 18000 + Ray cast hit results: tensor([[[-0.3698, 0.0357, 0.0000], + [-0.3698, 0.0357, 0.0000], + [-0.3698, 0.0357, 0.0000], + ..., + [ inf, inf, inf], + [ inf, inf, inf], + [ inf, inf, inf]]], device='cuda:0') + ------------------------------- + +Here we can see the data returned by the sensor itself. Notice first that there are 3 closed brackets at the beginning and the end: this is because the data returned is batched by the number of sensors. The ray cast pattern itself has also been flattened, and so the dimensions of the array are ``[N, B, 3]`` where ``N`` is the number of sensors, ``B`` is the number of cast rays in the pattern, and 3 is the dimension of the casting space. Finally, notice that the first several values in this casting pattern are the same: this is because the lidar pattern is spherical and we have specified our FOV to be hemispherical, which includes the poles. In this configuration, the "flattening pattern" becomes apparent: the first 180 entries will be the same because it's the bottom pole of this hemisphere, and there will be 180 of them because our horizontal FOV is 180 degrees with a resolution of 1 degree. + +You can use this script to experiment with pattern configurations and build an intuition about how the data is stored by altering the ``triggered`` variable on line 99. + +.. dropdown:: Code for raycaster_sensor.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/demos/sensors/raycaster_sensor.py + :language: python + :linenos: 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/docs/source/refs/reference_architecture/index.rst b/docs/source/refs/reference_architecture/index.rst new file mode 100644 index 0000000000..338b8a4415 --- /dev/null +++ b/docs/source/refs/reference_architecture/index.rst @@ -0,0 +1,375 @@ +Reference Architecture +====================== + +This document presents an overview of the end-to-end robot learning process with +Isaac Lab and Isaac Sim. This is demonstrated using a reference architecture that highlights +the major building blocks for training and deployment workflows. It provides a comprehensive, +user-friendly guide on the entire process of developing applications from training to deploying +the trained model in the real world, including links to demos, working examples, and documentation. + +Who is this document for? +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This document is designed to assist robotics developers and researchers working with NVIDIA Isaac Lab +in the robot learning field, including those at research labs, Original Equipment Manufacturers (OEM), +Solutions Providers, Solutions Integrators (SI), and independent software vendors (ISV). It offers +guidance on utilizing Isaac Lab’s robot training framework and workflows as a foundational starting +point for environment configuration, task design, and policy training and testing. + + + +.. image:: ../../_static/reference-architecture/isaac-lab-ra-light.svg + :class: only-light + :align: center + :alt: Isaac Lab Reference Architecture + +.. image:: ../../_static/reference-architecture/isaac-lab-ra-dark.svg + :class: only-dark + :align: center + :alt: Isaac Lab Reference Architecture + + +| + +The reference architecture for Isaac Lab comprises the following components: + +1. :ref:`Asset Input` +2. :ref:`Configuration - Assets & Scene` +3. :ref:`Robot Learning Task Design` +4. :ref:`Register with Gymnasium` +5. :ref:`Environment Wrapping` +6. :ref:`Run Training` +7. :ref:`Run Testing` + + + + +Components +~~~~~~~~~~~ +In this section, we will briefly discuss the individual blocks for creating a +sample reference application in Isaac Lab. + + +.. _ra-asset-input: + +Component 1 - Asset Input +--------------------------- +Isaac Lab accepts URDF, MJCF XML or USD files for the assets. The first step to training using Isaac Lab is to +have the USD file of your asset and the USD or URDF file of your robot. This can be achieved in +the following ways: + + +1. Design your assets or robot in Isaac Sim and export the USD file. + +2. Design your assets or robot in any software of your choice and export it to USD using Isaac Sim converters. Isaac Sim supports the different converters/importers to USD such as the `CAD Converter`_, `URDF Importer`_, `MJCF Importer`_, `Onshape Importer`_, etc. More details are found in the `Importing Assets section`_ in the `Isaac Sim Reference Architecture`_. + +3. If you already have the URDF or MJCF file of your robot, you do not need to convert to USD as Isaac Lab takes URDF and MJCF XML. + + +.. _ra-configuration: + +Component 2 - Configuration (Assets and Scene) +------------------------------------------------------ + +Asset Configuration +^^^^^^^^^^^^^^^^^^^^^^^^ + +Given that you have the asset file for your robot and other assets such as environment objects based on the task, the next step is to import them into Isaac Lab. Isaac Lab uses asset configuration classes to spawn various objects (or prims) into the scene using Python. The first step is to write a configuration class to define the properties for the assets needed to complete the task. For example, a simple go-to-goal task for a mobile robot will include the robot asset, an object like cubes to signify the goal pose visually, lights, ground plane, etc. Isaac Lab understands these assets using the configuration classes. Isaac Lab provides various sim-ready assets such as physically accurate +3D objects that encompass accurate physical properties and behavior. It also provides connected data streams to represent the real world in simulated digital worlds such as `robots `__ +like ANYbotics Anymal, Unitree H1 Humanoid, etc. as well as `sensors `__. We provide these assets configuration classes. Users can also define their own assets using the configuration classes. + +Follow the tutorial on `how to write an Articulation and ArticulationCfg class `__. + +Scene Configuration +^^^^^^^^^^^^^^^^^^^^^^^^ + +Given the individual asset configurations, the next step is to put all the assets together into a +scene. The scene configuration is a simple config class that initializes all the assets in the +scene that are needed for the task and for visualization. This is an example for the +`Cartpole example scene configuration `__, +which includes the cartpole, ground plane, and dome light. + + +.. _ra-robot-learning-task-design: + +Component 3 - Robot Learning Task Design +------------------------------------------------------ +Now, we have the scene for the task, but we need to define the robot learning task. We will focus on +`reinforcement learning (RL) `__ algorithm here. We define the RL task +that the agent is going to do. RL tasks are defined as a Markov Decision Process (MDP), +which is a stochastic decision-making process where optional decisions are made for the agents +considering their current state and environment they interact with. The environment provides the +agents’ current state or observations, and executes the actions provided by the agent. +The environment responds to the agents by providing the next states, reward of taking the +action, done flag and information about the current episode. Therefore, different components +of the MDP formulation (the environment) – states, actions, rewards, reset, done, etc. — must +be defined by the user for the agent to perform the given task. + +In Isaac Lab, we provide two different workflows for designing environments. + +Manager-based +^^^^^^^^^^^^^^^^^ +.. image:: ../../_static/task-workflows/manager-based-light.svg + :class: only-light + :align: center + :alt: Manager-based Task Workflow + +.. image:: ../../_static/task-workflows/manager-based-dark.svg + :class: only-dark + :align: center + :alt: Manager-based Task Workflow + +This workflow is modular, and the environment is decomposed into individual components (or managers) +that handle the different aspects of the environment, such as computing observations, +applying actions, and applying randomization. As a user, you define different configuration classes +for each component. + +- An RL task should have the following configuration classes: + + - Observations Config: Defines the agents’ observations for the task. + - Actions Config: Defines the agent’s action type, i.e. how the output of the agent are mapped to + the robot's control inputs. + - Rewards Config: Defines the reward function for the task + - Terminations Config: Defines the conditions for termination of an episode or when the task + is completed. + +- You can add other optional configuration classes such as Event Config which defines the set of randomizations and noisification for the agent and environment, Curriculum Config for tasks that require `curriculum learning`_ and Commands Config for tasks where the input is from a controller/setpoint controls e.g. a gamepad controller. + +.. tip:: + + To learn more on how you can design your own manager-based environment, see :ref:`tutorial-create-manager-rl-env`. + + + +Direct +^^^^^^^^ +.. image:: ../../_static/task-workflows/direct-based-light.svg + :class: only-light + :align: center + :alt: Direct-based Task Workflow + +.. image:: ../../_static/task-workflows/direct-based-dark.svg + :class: only-dark + :align: center + :alt: Direct-based Task Workflow + +In this workflow, you implement a single class that is responsible for computing observations, applying actions, and computing rewards. This workflow allows for direct control of the environment logic. + +.. tip:: + To learn more on how you can design your own direct environment, see :ref:`tutorial-create-direct-rl-env`. + +Users can choose from Isaac Lab’s large suite of pre-configured environments or users can define +their own environments. For more technical information about the two workflows, please see the +`documentation `__. + + +In addition to designing the RL task, you will need to design your agent’s model, the neural +network policy and value function. To train the RL agent to solve the task, you need to define +the hyperparameters such as number of epochs, learning rate, etc. for training and the +policy/value model architecture. This is defined in the training configuration file specific +to the RL library you want to use. Examples are created under the agent's folder in each task directory. +See an example of `RSL-RL `__ for Anymal-B. + + +.. _ra-register-gym: + +Component 4 - Register with Gymnasium +------------------------------------------------------ + +The next step is to register the environments with the gymnasium registry to allow you to create the environment using the unique environment name. +Registration is a way to make the environment accessible and reusable across different +RL algorithms and experiments. This is common in the RL community. Follow the tutorial on +`Registering an Environment `__ to learn more about how to register in your own environment. + +.. _ra-env-wrap: + +Component 5 - Environment Wrapping +------------------------------------------------------ +In running your RL task, you might want to change the behavior of your environment without +changing the environment itself. For example, you might want to create functions to modify +observations or rewards, record videos, or enforce time limits. Isaac Lab utilizes the API +available in the `gymnasium.Wrapper `__ class to create interfaces to the simulated environments. + +Some wrappers include: + +* `Video Wrappers `__ +* `RL Libraries Wrappers `__ + +Most RL libraries expect their own variation of an environment interface. This means the +data types needed by each library differs. Isaac Lab provides its own wrappers to convert +the environment into the expected interface by the RL library a user wants to use. These are +specified in the `Isaac Lab utils wrapper module `__. + +See the `full list `__ of other wrappers APIs. For more information on how these wrappers work, +please refer to the `Wrapping environments `__ documentation. + +Adding your own wrappers +^^^^^^^^^^^^^^^^^^^^^^^^ + +You can define your own wrappers by adding them to the Isaac Lab utils wrapper module. More information is available `on the GitHub page for wrapping environments `__. + +.. _ra-run-training: + +Component 6 - Run Training +--------------------------- + +Finally, the last step is to run the training of the RL agent. Isaac Lab provides scripts which utilizes four popular RL libraries for training the models (GPU-based training): + +* `StableBaselines3 `__ +* `RSL-RL `__ +* `RL-Games `__ +* `SKRL `__ + + +.. note:: + + Isaac Lab does not provide the implementation of these RL libraries. They are already implemented by different authors. We provide the environments and framework wrappers for the RL libraries. + + + +If you want to integrate a different version of the provided algorithms or your learning library, you can follow +`these instructions `__. + + + +Single GPU Training +^^^^^^^^^^^^^^^^^^^^^^^^ +.. image:: ../../_static/reference-architecture/single-gpu-training-light.svg + :class: only-light + :align: center + :alt: Single GPU Training Data Flow + +.. image:: ../../_static/reference-architecture/single-gpu-training-dark.svg + :class: only-dark + :align: center + :alt: Single GPU Training Data Flow + +Isaac Lab supports training massively parallel environments to speed up RL training and provides rich data for the model to train. +For single GPU training, the following steps show how training works in Isaac Sim and Isaac Lab: + +1. **In Isaac Sim** + +* Isaac Sim provides the asset states such as robot and sensor states, including the observations defined in the task observation config class. + +2. **In Isaac Lab** + +* Randomizations are added to the states defined in the event configuration class to obtain the observation for the task. Randomizations are however optional. If not defined, the states are the observations. +* The observations are computed as PyTorch tensors, and it can optionally include the action provided by the trained model based on the task. + +3. **In the RL library** + +* The observation is passed to the policy. +* The policy is trained to output the right actions for the robot using RL library algorithms such as PPO, TRPO, etc. +* The actions can serve either as a setpoint for a controller that generates the action to the robot or used directly as the action to the robot based on the task. +* Action types such as joint position for a quadruped is an input to a joint controller, velocity of 1 or 0 is used to control the cart directly in the cartpole task, etc. +* In addition, based on how the task is defined, the previous action can be part of the next set of observations that is sent. + +4. **In Isaac Sim** + +* The actions from the policy are sent back to Isaac Sim to control the agent that is learning i.e. the robot. This is the physics simulation (sim) step. This generates the next states in Isaac Sim and the rewards are calculated in Isaac Lab. + +5. **Rendering** + +* The scene can be rendered to produce the cameras' images. + + +The next state is then passed in the flow till the training reaches the specified training steps or epochs. The final product is the trained model/agent. + + + +Multi-GPU and Multi-Node Training +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. image:: ../../_static/reference-architecture/multi-gpu-training-light.svg + :class: only-light + :align: center + :alt: Multi GPU Training Data Flow + +.. image:: ../../_static/reference-architecture/multi-gpu-training-dark.svg + :class: only-dark + :align: center + :alt: Multi GPU Training Data Flow + + +Isaac Lab supports scaling up training by taking advantage of multi-GPU and multi-node training on Linux. Follow the tutorial on `Multi-GPU training `__ and `Multi-Node training `__ to get started. + + +Cloud-Based Training +^^^^^^^^^^^^^^^^^^^^^^^^ +Isaac Lab can be deployed alongside Isaac Sim onto the public clouds with `Isaac Automator `__. AWS, GCP, Azure, and Alibaba Cloud are currently supported. Follow the tutorial on `how to run Isaac Lab in the cloud `__. + +.. note:: + + Both multi-GPU and multi-node jobs can be easily scaled across heterogeneous environments with `OSMO `__, a cloud-native, orchestration platform for scheduling complex multi-stage and multi-container heterogeneous computing workflows. Isaac Lab also provides the tools to run your RL task in Docker. See more details on `container deployment `__. + +.. _ra-run-testing: + +Component 7: Run Testing +----------------------------- +Isaac Lab provides scripts for `testing/playing the trained policy `__ on the environment and functions for converting the trained model from .pt to +.jit and .onnx for deployment. + + +Deployment on Physical Robots +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../../_static/reference-architecture/deployment-light.svg + :class: only-light + :align: center + :alt: Isaac Lab Trained Policy Deployment + +.. image:: ../../_static/reference-architecture/deployment-dark.svg + :class: only-dark + :align: center + :alt: Isaac Lab Trained Policy Deployment + + +To deploy your trained model on a real robot, you would need what is shown in the flow diagram. Note, this is a sample reference architecture, hence it can be tweaked for a different application. +First, you need a robot with the required sensors and processing computer such as `NVIDIA Jetson `__ to deploy on. Next, you need a state estimator for your robot. The state estimator should be able to deliver the list of observations used for training. + +Once the observations are extracted, they are passed into the model which delivers the action using the model inferencing runtime. The commanded action from the model serves as setpoints for the action controller. The action controller outputs scaled actions which are then used to control the robot to get to the next state, and this continues till the task is done. + +NVIDIA Isaac platform provides some tools for state estimation, including visual slam and inferencing engines such as `TensorRT `__. Other inferencing runtime includes `OnnxRuntime `__, direct inferencing on the PyTorch model, etc. + + + + +Summary +~~~~~~~~~~~ + +This document presents a reference architecture for Isaac Lab that has undergone SQA testing. We have provided a user-friendly guide to end-to-end robot learning with Isaac Lab and Isaac Sim from training to real-world deployment, including demos, examples, and documentation links. + + +How to Get Started +~~~~~~~~~~~~~~~~~~~~~~ +Check out our resources on using Isaac Lab with your robots. + +Review Our Documentation & Samples Resources + +* `Isaac Lab Tutorials`_ +* `Fast-Track Robot Learning in Simulation Using NVIDIA Isaac Lab`_ +* `Supercharge Robotics Workflows with AI and Simulation Using NVIDIA Isaac Sim 4.0 and NVIDIA Isaac Lab`_ +* `Closing the Sim-to-Real Gap: Training Spot Quadruped Locomotion with NVIDIA Isaac Lab `__ +* `Additional Resources`_ + +Learn More About Featured NVIDIA Solutions + +* `Scale AI-Enabled Robotics Development Workloads with NVIDIA OSMO`_ +* `Parkour and More: How Simulation-Based RL Helps to Push the Boundaries in Legged Locomotion (GTC session) `__ +* `Isaac Perceptor`_ +* `Isaac Manipulator`_ + +.. _curriculum learning: https://arxiv.org/abs/2109.11978 +.. _CAD Converter: https://docs.omniverse.nvidia.com/extensions/latest/ext_cad-converter.html +.. _URDF Importer: https://docs.omniverse.nvidia.com/isaacsim/latest/advanced_tutorials/tutorial_advanced_import_urdf.html +.. _MJCF Importer: https://docs.omniverse.nvidia.com/isaacsim/latest/advanced_tutorials/tutorial_advanced_import_mjcf.html#import-mjcf +.. _Onshape Importer: https://docs.omniverse.nvidia.com/extensions/latest/ext_onshape.html +.. _Isaac Sim Reference Architecture: https://docs.omniverse.nvidia.com/isaacsim/latest/isaac_sim_reference_architecture.html +.. _Importing Assets section: https://docs.omniverse.nvidia.com/isaacsim/latest/isaac_sim_reference_architecture.html#importing-assets + +.. _Scale AI-Enabled Robotics Development Workloads with NVIDIA OSMO: https://developer.nvidia.com/blog/scale-ai-enabled-robotics-development-workloads-with-nvidia-osmo/ +.. _Isaac Perceptor: https://developer.nvidia.com/isaac/perceptor +.. _Isaac Manipulator: https://developer.nvidia.com/isaac/manipulator +.. _Additional Resources: https://isaac-sim.github.io/IsaacLab/main/source/refs/additional_resources.html +.. _Isaac Lab Tutorials: file:///home/oomotuyi/isaac/IsaacLab/docs/_build/current/source/tutorials/index.html +.. _Fast-Track Robot Learning in Simulation Using NVIDIA Isaac Lab: https://developer.nvidia.com/blog/fast-track-robot-learning-in-simulation-using-nvidia-isaac-lab/ +.. _Supercharge Robotics Workflows with AI and Simulation Using NVIDIA Isaac Sim 4.0 and NVIDIA Isaac Lab: https://developer.nvidia.com/blog/supercharge-robotics-workflows-with-ai-and-simulation-using-nvidia-isaac-sim-4-0-and-nvidia-isaac-lab/ diff --git a/docs/source/refs/snippets/tutorial_modify_direct_rl_env.py b/docs/source/refs/snippets/tutorial_modify_direct_rl_env.py index 8073886840..215ef3bd4f 100644 --- a/docs/source/refs/snippets/tutorial_modify_direct_rl_env.py +++ b/docs/source/refs/snippets/tutorial_modify_direct_rl_env.py @@ -28,8 +28,8 @@ # [end-h1_env-import] # [start-h1_env-spaces] -num_actions = 19 -num_observations = 69 +action_space = 19 +observation_space = 69 # [end-h1_env-spaces] # [start-h1_env-robot] diff --git a/docs/source/refs/troubleshooting.rst b/docs/source/refs/troubleshooting.rst index 09830e8812..91c251c373 100644 --- a/docs/source/refs/troubleshooting.rst +++ b/docs/source/refs/troubleshooting.rst @@ -35,6 +35,26 @@ In the above example, the log file is located at ``.../logs/Kit/Isaac-Sim/2023.1 You can open this file to check the internal logs from the simulator. Also when reporting issues, please include this log file to help us debug the issue. +Changing logging channel levels for the simulator +------------------------------------------------- + +By default, the simulator logs messages at the ``WARN`` level and above on the terminal. You can change the logging +channel levels to get more detailed logs. The logging channel levels can be set through Omniverse's logging system. + +To obtain more detailed logs, you can run your application with the following flags: + +* ``--info``: This flag logs messages at the ``INFO`` level and above. +* ``--verbose``: This flag logs messages at the ``VERBOSE`` level and above. + +For instance, to run a standalone script with verbose logging, you can use the following command: + +.. code-block:: bash + + # Run the standalone script with info logging + ./isaaclab.sh -p source/standalone/tutorials/00_sim/create_empty.py --headless --info + +For more fine-grained control, you can modify the logging channels through the ``omni.log`` module. +For more information, please refer to its `documentation `__. Using CPU Scaling Governor for performance ------------------------------------------ diff --git a/docs/source/setup/ecosystem.rst b/docs/source/setup/ecosystem.rst new file mode 100644 index 0000000000..107c95843d --- /dev/null +++ b/docs/source/setup/ecosystem.rst @@ -0,0 +1,32 @@ +Isaac Lab Ecosystem +=================== + +Isaac Lab is built on top of Isaac Sim to provide a unified and flexible framework +for robot learning that exploits latest simulation technologies. It is designed to be modular and extensible, +and aims to simplify common workflows in robotics research (such as RL, learning from demonstrations, and +motion planning). While it includes some pre-built environments, sensors, and tasks, its main goal is to +provide an open-sourced, unified, and easy-to-use interface for developing and testing custom environments +and robot learning algorithms. + +Working with Isaac Lab requires the installation of Isaac Sim, which is packaged with core robotics tools +that Isaac Lab depends on, including URDF and MJCF importers, simulation managers, and ROS features. Isaac +Sim also builds on top of the Nvidia Omniverse platform, leveraging advanced physics simulation from PhysX, +photorealistic rendering technologies, and Universal Scene Description (USD) for scene creation. + +Isaac Lab not only inherits the capabilities of Isaac Sim, but also adds a number +of new features that pertain to robot learning research. For example, including actuator dynamics in the +simulation, procedural terrain generation, and support to collect data from human demonstrations. + +.. image:: ../_static/setup/ecosystem-light.jpg + :class: only-light + :align: center + :alt: The Isaac Lab, Isaac Sim, and Nvidia Omniverse ecosystem + +.. image:: ../_static/setup/ecosystem-dark.jpg + :class: only-dark + :align: center + :alt: The Isaac Lab, Isaac Sim, and Nvidia Omniverse ecosystem + + +For a detailed explanation of Nvidia's development journey of robot learning frameworks, please visit +the `FAQ page `_. diff --git a/docs/source/setup/installation/binaries_installation.rst b/docs/source/setup/installation/binaries_installation.rst index e9c4469bca..73138ebd17 100644 --- a/docs/source/setup/installation/binaries_installation.rst +++ b/docs/source/setup/installation/binaries_installation.rst @@ -38,7 +38,7 @@ To check the minimum system requirements,refer to the documentation :sync: windows On Windows systems, by default,Isaac Sim is installed in the directory - ``C:\Users\%USERPROFILE%\AppData\Local\ov\pkg\isaac_sim-*``, with ``*`` corresponding to the Isaac Sim version. + ``%USERPROFILE%\AppData\Local\ov\pkg\isaac_sim-*``, with ``*`` corresponding to the Isaac Sim version. Verifying the Isaac Sim installation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -66,7 +66,7 @@ variables to your terminal for the remaining of the installation instructions: .. code:: batch :: Isaac Sim root directory - set ISAACSIM_PATH="C:\Users\%USERPROFILE%\AppData\Local\ov\pkg\isaac-sim-4.2.0" + set ISAACSIM_PATH="%USERPROFILE%\AppData\Local\ov\pkg\isaac-sim-4.2.0" :: Isaac Sim python executable set ISAACSIM_PYTHON_EXE="%ISAACSIM_PATH:"=%\python.bat" @@ -407,8 +407,14 @@ top of the repository: The above command should launch the simulator and display a window with a black -ground plane. You can exit the script by pressing ``Ctrl+C`` on your terminal. +viewport. You can exit the script by pressing ``Ctrl+C`` on your terminal. On Windows machines, please terminate the process from Command Prompt using ``Ctrl+Break`` or ``Ctrl+fn+B``. +.. figure:: ../../_static/setup/verify_install.jpg + :align: center + :figwidth: 100% + :alt: Simulator with a black window. + + If you see this, then the installation was successful! |:tada:| diff --git a/docs/source/setup/installation/index.rst b/docs/source/setup/installation/index.rst index cae19383b2..90c8937cfc 100644 --- a/docs/source/setup/installation/index.rst +++ b/docs/source/setup/installation/index.rst @@ -1,5 +1,5 @@ -Installation Guide -=================== +Local Installation +================== .. image:: https://img.shields.io/badge/IsaacSim-4.2.0-silver.svg :target: https://developer.nvidia.com/isaac-sim @@ -31,14 +31,9 @@ Installation Guide For the full list of system requirements for Isaac Sim, please refer to the `Isaac Sim system requirements `_. -As an experimental feature since Isaac Sim 4.0 release, Isaac Sim can also be installed through pip. -This simplifies the installation -process by avoiding the need to download the Omniverse Launcher and installing Isaac Sim through -the launcher. Therefore, there are two ways to install Isaac Lab: .. toctree:: :maxdepth: 2 - Option 1: Installation using Isaac Sim pip - Option 2: Installation using Isaac Sim binaries - cloud_installation + Pip installation (recommended for Ubuntu 22.04 and Windows) + Binary installation (recommended for Ubuntu 20.04) diff --git a/docs/source/setup/installation/pip_installation.rst b/docs/source/setup/installation/pip_installation.rst index cc014fd887..83a74194ce 100644 --- a/docs/source/setup/installation/pip_installation.rst +++ b/docs/source/setup/installation/pip_installation.rst @@ -88,7 +88,14 @@ compatibility issues with some Linux distributions. If you encounter any issues, pip install --upgrade pip -- Then, install the Isaac Sim packages necessary for running Isaac Lab: +- Then, install the Isaac Sim packages + + .. code-block:: bash + + pip install isaacsim==4.2.0.2 isaacsim-extscache-physics==4.2.0.2 isaacsim-extscache-kit==4.2.0.2 isaacsim-extscache-kit-sdk==4.2.0.2 --extra-index-url https://pypi.nvidia.com + + +- To install a minimal set of packages for running Isaac Lab only, the following command can be used. Note that you cannot run ``isaacsim`` with this. .. code-block:: bash @@ -310,8 +317,15 @@ top of the repository: The above command should launch the simulator and display a window with a black -ground plane. You can exit the script by pressing ``Ctrl+C`` on your terminal. +viewport as shown below. You can exit the script by pressing ``Ctrl+C`` on your terminal. On Windows machines, please terminate the process from Command Prompt using ``Ctrl+Break`` or ``Ctrl+fn+B``. + +.. figure:: ../../_static/setup/verify_install.jpg + :align: center + :figwidth: 100% + :alt: Simulator with a black window. + + If you see this, then the installation was successful! |:tada:| diff --git a/docs/source/tutorials/03_envs/create_direct_rl_env.rst b/docs/source/tutorials/03_envs/create_direct_rl_env.rst index ab5136106a..a4b945be9d 100644 --- a/docs/source/tutorials/03_envs/create_direct_rl_env.rst +++ b/docs/source/tutorials/03_envs/create_direct_rl_env.rst @@ -48,9 +48,9 @@ config should define the number of actions and observations for the environment. @configclass class CartpoleEnvCfg(DirectRLEnvCfg): ... - num_actions = 1 - num_observations = 4 - num_states = 0 + action_space = 1 + observation_space = 4 + state_space = 0 The config class can also be used to define task-specific attributes, such as scaling for reward terms and thresholds for reset conditions. 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/isaaclab.bat b/isaaclab.bat index b415ef1a13..09c6818e37 100644 --- a/isaaclab.bat +++ b/isaaclab.bat @@ -519,9 +519,9 @@ if "%arg%"=="-i" ( call :extract_python_exe pushd %ISAACLAB_PATH%\docs call !python_exe! -m pip install -r requirements.txt >nul - call !python_exe! -m sphinx -b html -d _build\doctrees . _build\html + call !python_exe! -m sphinx -b html -d _build\doctrees . _build\current echo [INFO] To open documentation on default browser, run: - echo xdg-open "%ISAACLAB_PATH%\docs\_build\html\index.html" + echo xdg-open "%ISAACLAB_PATH%\docs\_build\current\index.html" popd >nul shift goto :end diff --git a/isaaclab.sh b/isaaclab.sh index a604706e70..eedf6636ed 100755 --- a/isaaclab.sh +++ b/isaaclab.sh @@ -328,7 +328,7 @@ while [[ $# -gt 0 ]]; do fi # run the formatter over the repository # check if pre-commit is installed - if [ ! command -v pre-commit &>/dev/null ]; then + if ! command -v pre-commit &>/dev/null; then echo "[INFO] Installing pre-commit..." pip install pre-commit fi @@ -350,7 +350,7 @@ while [[ $# -gt 0 ]]; do python_exe=$(extract_python_exe) echo "[INFO] Using python from: ${python_exe}" shift # past argument - ${python_exe} $@ + ${python_exe} "$@" # exit neatly break ;; @@ -396,10 +396,10 @@ while [[ $# -gt 0 ]]; do cd ${ISAACLAB_PATH}/docs ${python_exe} -m pip install -r requirements.txt > /dev/null # build the documentation - ${python_exe} -m sphinx -b html -d _build/doctrees . _build/html + ${python_exe} -m sphinx -b html -d _build/doctrees . _build/current # open the documentation echo -e "[INFO] To open documentation on default browser, run:" - echo -e "\n\t\txdg-open $(pwd)/_build/html/index.html\n" + echo -e "\n\t\txdg-open $(pwd)/_build/current/index.html\n" # exit neatly cd - > /dev/null shift # past argument diff --git a/pyproject.toml b/pyproject.toml index 51d4375907..63ec9afd2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,9 @@ extra_standard_library = [ "toml", "trimesh", "tqdm", + "torchvision", + "transformers", + "einops" # Needed for transformers, doesn't always auto-install ] # Imports from Isaac Sim and Omniverse known_third_party = [ diff --git a/source/apps/isaaclab.python.headless.kit b/source/apps/isaaclab.python.headless.kit index 9eace6a1a6..9d13464ecf 100644 --- a/source/apps/isaaclab.python.headless.kit +++ b/source/apps/isaaclab.python.headless.kit @@ -5,7 +5,7 @@ [package] title = "Isaac Lab Python Headless" description = "An app for running Isaac Lab headlessly" -version = "1.2.0" +version = "1.3.0" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "isaaclab", "python", "headless"] @@ -34,6 +34,10 @@ renderer.active = "rtx" app.content.emptyStageOnStart = false +# Disable print outs on extension startup information +# this only disables the app print_and_log function +app.enableStdoutOutput = false + # Setting the port for the embedded http server exts."omni.services.transport.server.http".port = 8211 diff --git a/source/apps/isaaclab.python.headless.rendering.kit b/source/apps/isaaclab.python.headless.rendering.kit index 5d14c551dc..cd39d0a797 100644 --- a/source/apps/isaaclab.python.headless.rendering.kit +++ b/source/apps/isaaclab.python.headless.rendering.kit @@ -9,7 +9,7 @@ [package] title = "Isaac Lab Python Headless Camera" description = "An app for running Isaac Lab headlessly with rendering enabled" -version = "1.2.0" +version = "1.3.0" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "isaaclab", "python", "camera", "minimal"] @@ -35,6 +35,10 @@ app.folder = "${exe-path}/" app.name = "Isaac-Sim" app.version = "4.2.0" +# Disable print outs on extension startup information +# this only disables the app print_and_log function +app.enableStdoutOutput = false + # set the default ros bridge to disable on startup isaac.startup.ros_bridge_extension = "" diff --git a/source/apps/isaaclab.python.kit b/source/apps/isaaclab.python.kit index df788530fa..7ea6fc1a05 100644 --- a/source/apps/isaaclab.python.kit +++ b/source/apps/isaaclab.python.kit @@ -5,7 +5,7 @@ [package] title = "Isaac Lab Python" description = "An app for running Isaac Lab" -version = "1.2.0" +version = "1.3.0" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "usd"] @@ -124,6 +124,10 @@ exts."omni.kit.menu.utils".logDeprecated = false # app.content.emptyStageOnStart = false app.file.ignoreUnsavedOnExit = true # prevents save dialog when exiting +# disable print outs on extension startup information +# this only disables the app print_and_log function +app.enableStdoutOutput = false + # deprecate support for old kit.ui.menu app.menu.legacy_mode = false # use omni.ui.Menu for the MenuBar @@ -137,6 +141,9 @@ exts."omni.kit.window.viewport".blockingGetViewportDrawable = false exts."omni.kit.test".includeTests = [ "*isaac*" ] +# set the default ros bridge to disable on startup +isaac.startup.ros_bridge_extension = "" + [settings.app.python] # These disable the kit app from also printing out python output, which gets confusing interceptSysStdOutput = false diff --git a/source/apps/isaaclab.python.rendering.kit b/source/apps/isaaclab.python.rendering.kit index 539b109a19..38a48b7460 100644 --- a/source/apps/isaaclab.python.rendering.kit +++ b/source/apps/isaaclab.python.rendering.kit @@ -9,7 +9,7 @@ [package] title = "Isaac Lab Python Camera" description = "An app for running Isaac Lab with rendering enabled" -version = "1.2.0" +version = "1.3.0" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "isaaclab", "python", "camera", "minimal"] @@ -35,6 +35,10 @@ app.folder = "${exe-path}/" app.name = "Isaac-Sim" app.version = "4.2.0" +# Disable print outs on extension startup information +# this only disables the app print_and_log function +app.enableStdoutOutput = false + # set the default ros bridge to disable on startup isaac.startup.ros_bridge_extension = "" diff --git a/source/extensions/omni.isaac.lab/config/extension.toml b/source/extensions/omni.isaac.lab/config/extension.toml index 473b7a0986..082ec35c24 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.27.25" # Description title = "Isaac Lab framework for Robot Learning" @@ -13,14 +13,12 @@ keywords = ["kit", "robotics", "learning", "ai"] [dependencies] "omni.isaac.core" = {} -"omni.isaac.ml_archive" = {} "omni.replicator.core" = {} [python.pipapi] requirements = [ "numpy", "prettytable==3.3.0", - "tensordict", "toml", "hidapi", "gymnasium==0.29.0", @@ -30,7 +28,6 @@ requirements = [ modules = [ "numpy", "prettytable", - "tensordict", "toml", "hid", "gymnasium", diff --git a/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst b/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst index 2e67c3708c..7ff5ef82d4 100644 --- a/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst +++ b/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst @@ -1,6 +1,342 @@ Changelog --------- +0.27.25 (2024-12-11) +~~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Modified :class:`omni.isaac.lab.envs.mdp.actions.DifferentialInverseKinematicsAction` class to use the geometric + Jacobian computed w.r.t. to the root frame of the robot. This helps ensure that root pose does not affect the tracking. + + +0.27.24 (2024-12-09) +~~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed the initial state recorder term in :class:`omni.isaac.lab.envs.mdp.recorders.InitialStateRecorder` to + return only the states of the specified environment IDs. + + +0.27.23 (2024-12-06) +~~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed the enforcement of :attr:`~omni.isaac.lab.actuators.ActuatorBaseCfg.velocity_limits` at the + :attr:`~omni.isaac.lab.assets.Articulation.root_physx_view` level. + + +0.27.22 (2024-12-06) +~~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* If a USD that contains an articulation root is loaded using a + :attr:`omni.isaac_lab.assets.RigidBody` we now fail unless the articulation root is explicitly + disabled. Using an articulation root for rigid bodies is not needed and decreases overall performance. + + +0.27.21 (2024-12-06) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Corrected the projection types of fisheye camera in :class:`omni.isaac.lab.sim.spawners.sensors.sensors_cfg.FisheyeCameraCfg`. + Earlier, the projection names used snakecase instead of camelcase. + + +0.27.20 (2024-12-06) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added option to define the clipping behavior for depth images generated by + :class:`~omni.isaac.lab.sensors.RayCasterCamera`, :class:`~omni.isaac.lab.sensors.Camera`, and :class:`~omni.isaac.lab.sensors.TiledCamera` + +Changed +^^^^^^^ + +* Unified the clipping behavior for the depth images of all camera implementations. Per default, all values exceeding + the range are clipped to zero for both ``distance_to_image_plane`` and ``distance_to_camera`` depth images. Prev. + :class:`~omni.isaac.lab.sensors.RayCasterCamera` clipped the values to the maximum value of the depth image, + :class:`~omni.isaac.lab.sensors.Camera` did not clip them and had a different behavior for both types. + + +0.27.19 (2024-12-05) +~~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed the condition in ``isaaclab.sh`` that checks whether ``pre-commit`` is installed before attempting installation. + + +0.27.18 (2024-12-04) +~~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed the order of the incoming parameters in :class:`omni.isaac.lab.envs.DirectMARLEnv` to correctly use ``NoiseModel`` in marl-envs. + + +0.27.17 (2024-12-02) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :class:`~omni.isaac.lab.managers.RecorderManager` and its utility classes to record data from the simulation. +* Added :class:`~omni.isaac.lab.utils.datasets.EpisodeData` to store data for an episode. +* Added :class:`~omni.isaac.lab.utils.datasets.DatasetFileHandlerBase` as a base class for handling dataset files. +* Added :class:`~omni.isaac.lab.utils.datasets.HDF5DatasetFileHandler` as a dataset file handler implementation to + export and load episodes from HDF5 files. +* Added ``record_demos.py`` script to record human-teleoperated demos for a specified task and export to an HDF5 file. +* Added ``replay_demos.py`` script to replay demos loaded from an HDF5 file. + + +0.27.16 (2024-11-21) +~~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Changed :class:`omni.isaac.lab.envs.DirectMARLEnv` to inherit from ``Gymnasium.Env`` due to requirement from Gymnasium v1.0.0 requiring all environments to be a subclass of ``Gymnasium.Env`` when using the ``make`` interface. + + +0.27.15 (2024-11-09) +~~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed indexing in :meth:`omni.isaac.lab.assets.Articulation.write_joint_limits_to_sim` to correctly process non-None ``env_ids`` and ``joint_ids``. + + +0.27.14 (2024-10-23) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added the class :class:`~omni.isaac.lab.assets.RigidObjectCollection` which allows to spawn + multiple objects in each environment and access/modify the quantities with a unified (env_ids, object_ids) API. + + +0.27.13 (2024-10-30) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added the attributes :attr:`~omni.isaac.lab.sim.converters.MeshConverterCfg.translation`, :attr:`~omni.isaac.lab.sim.converters.MeshConverterCfg.rotation`, + :attr:`~omni.isaac.lab.sim.converters.MeshConverterCfg.scale` to translate, rotate, and scale meshes + when importing them with :class:`~omni.isaac.lab.sim.converters.MeshConverter`. + + +0.27.12 (2024-01-04) +~~~~~~~~~~~~~~~~~~~~ + +Removed +^^^^^^^ + +* Removed TensorDict usage in favor of Python dictionary in sensors + + +0.27.11 (2024-10-31) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added support to define tuple of floats to scale observation terms by expanding the + :attr:`omni.isaac.lab.managers.manager_term_cfg.ObservationManagerCfg.scale` attribute. + + +0.27.10 (2024-11-01) +~~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Cached the PhysX view's joint paths before looping over them when processing fixed joint tendons + inside the :class:`Articulation` class. This helps improve the processing time for the tendons. + + +0.27.9 (2024-11-01) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added the :class:`omni.isaac.lab.utils.types.ArticulationActions` class to store the joint actions + for an articulation. Earlier, the class from Isaac Sim was being used. However, it used a different + type for the joint actions which was not compatible with the Isaac Lab framework. + + +0.27.8 (2024-11-01) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Added sanity check if the term is a valid type inside the command manager. +* Corrected the iteration over ``group_cfg_items`` inside the observation manager. + + +0.27.7 (2024-10-28) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added frozen encoder feature extraction observation space with ResNet and Theia + + +0.27.6 (2024-10-25) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed usage of ``meshes`` property in :class:`omni.isaac.lab.sensors.RayCasterCamera` to use ``self.meshes`` instead of the undefined ``RayCaster.meshes``. +* Fixed issue in :class:`omni.isaac.lab.envs.ui.BaseEnvWindow` where undefined configs were being accessed when creating debug visualization elements in UI. + + +0.27.5 (2024-10-25) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added utilities for serializing/deserializing Gymnasium spaces. + + +0.27.4 (2024-10-18) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Updated installation path instructions for Windows in the Isaac Lab documentation to remove redundancy in the use of %USERPROFILE% for path definitions. + + +0.27.3 (2024-10-22) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed the issue with using list or tuples of ``configclass`` within a ``configclass``. Earlier, the list of + configclass objects were not converted to dictionary properly when ``to_dict`` function was called. + + +0.27.2 (2024-10-21) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added ``--kit_args`` to :class:`~omni.isaac.lab.app.AppLauncher` to allow passing command line arguments directly to Omniverse Kit SDK. + + +0.27.1 (2024-10-20) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :class:`~omni.isaac.lab.sim.RenderCfg` and the attribute :attr:`~omni.isaac.lab.sim.SimulationCfg.render` for + specifying render related settings. + + +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) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added Imu sensor implementation that directly accesses the physx view :class:`omni.isaac.lab.sensors.Imu`. The + sensor comes with a configuration class :class:`omni.isaac.lab.sensors.ImuCfg` and data class + :class:`omni.isaac.lab.sensors.ImuData`. +* Moved and renamed :meth:`omni.isaac.lab.sensors.camera.utils.convert_orientation_convention` to :meth:`omni.isaac.lab.utils.math.convert_camera_frame_orientation_convention` +* Moved :meth:`omni.isaac.lab.sensors.camera.utils.create_rotation_matrix_from_view` to :meth:`omni.isaac.lab.utils.math.create_rotation_matrix_from_view` + + +0.25.2 (2024-10-16) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added support for different Gymnasium spaces (``Box``, ``Discrete``, ``MultiDiscrete``, ``Tuple`` and ``Dict``) + to define observation, action and state spaces in the direct workflow. +* Added :meth:`sample_space` to environment utils to sample supported spaces where data containers are torch tensors. + +Changed +^^^^^^^ + +* Mark the :attr:`num_observations`, :attr:`num_actions` and :attr:`num_states` in :class:`DirectRLEnvCfg` as deprecated + in favor of :attr:`observation_space`, :attr:`action_space` and :attr:`state_space` respectively. +* Mark the :attr:`num_observations`, :attr:`num_actions` and :attr:`num_states` in :class:`DirectMARLEnvCfg` as deprecated + in favor of :attr:`observation_spaces`, :attr:`action_spaces` and :attr:`state_space` respectively. + + +0.25.1 (2024-10-10) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed potential issue where default joint positions can fall outside of the limits being set with Articulation's + ``write_joint_limits_to_sim`` API. + + +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) +~~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* 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/actuators/actuator_base.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/actuators/actuator_base.py index 4d53fc048b..fba943bc15 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/actuators/actuator_base.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/actuators/actuator_base.py @@ -10,9 +10,8 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -from omni.isaac.core.utils.types import ArticulationActions - import omni.isaac.lab.utils.string as string_utils +from omni.isaac.lab.utils.types import ArticulationActions if TYPE_CHECKING: from .actuator_cfg import ActuatorBaseCfg @@ -207,6 +206,7 @@ def _parse_joint_parameter( TypeError: If the parameter value is not of the expected type. TypeError: If the default value is not of the expected type. ValueError: If the parameter value is None and no default value is provided. + ValueError: If the default value tensor is the wrong shape. """ # create parameter buffer param = torch.zeros(self._num_envs, self.num_joints, device=self._device) @@ -221,16 +221,29 @@ def _parse_joint_parameter( # note: need to specify type to be safe (e.g. values are ints, but we want floats) param[:, indices] = torch.tensor(values, dtype=torch.float, device=self._device) else: - raise TypeError(f"Invalid type for parameter value: {type(cfg_value)}. Expected float or dict.") + raise TypeError( + f"Invalid type for parameter value: {type(cfg_value)} for " + + f"actuator on joints {self.joint_names}. Expected float or dict." + ) elif default_value is not None: if isinstance(default_value, (float, int)): # if float, then use the same value for all joints param[:] = float(default_value) elif isinstance(default_value, torch.Tensor): # if tensor, then use the same tensor for all joints - param[:] = default_value.float() + if default_value.shape == (self._num_envs, self.num_joints): + param = default_value.float() + else: + raise ValueError( + "Invalid default value tensor shape.\n" + f"Got: {default_value.shape}\n" + f"Expected: {(self._num_envs, self.num_joints)}" + ) else: - raise TypeError(f"Invalid type for default value: {type(default_value)}. Expected float or Tensor.") + raise TypeError( + f"Invalid type for default value: {type(default_value)} for " + + f"actuator on joints {self.joint_names}. Expected float or Tensor." + ) else: raise ValueError("The parameter value is None and no default value is provided.") diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/actuators/actuator_net.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/actuators/actuator_net.py index 5cf034679d..c0d440533a 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/actuators/actuator_net.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/actuators/actuator_net.py @@ -18,9 +18,8 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -from omni.isaac.core.utils.types import ArticulationActions - from omni.isaac.lab.utils.assets import read_file +from omni.isaac.lab.utils.types import ArticulationActions from .actuator_pd import DCMotor diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/actuators/actuator_pd.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/actuators/actuator_pd.py index 165567536f..563b29ba47 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/actuators/actuator_pd.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/actuators/actuator_pd.py @@ -9,9 +9,8 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -from omni.isaac.core.utils.types import ArticulationActions - from omni.isaac.lab.utils import DelayBuffer, LinearInterpolation +from omni.isaac.lab.utils.types import ArticulationActions from .actuator_base import ActuatorBase @@ -63,7 +62,22 @@ def reset(self, *args, **kwargs): def compute( self, control_action: ArticulationActions, joint_pos: torch.Tensor, joint_vel: torch.Tensor ) -> ArticulationActions: - """Compute the aproximmate torques for the actuated joint (physX does not compute this explicitly).""" + """Process the actuator group actions and compute the articulation actions. + + In case of implicit actuator, the control action is directly returned as the computed action. + This function is a no-op and does not perform any computation on the input control action. + However, it computes the approximate torques for the actuated joint since PhysX does not compute + this quantity explicitly. + + Args: + control_action: The joint action instance comprising of the desired joint positions, joint velocities + and (feed-forward) joint efforts. + joint_pos: The current joint positions of the joints in the group. Shape is (num_envs, num_joints). + joint_vel: The current joint velocities of the joints in the group. Shape is (num_envs, num_joints). + + Returns: + The computed desired joint positions, joint velocities and joint efforts. + """ # store approximate torques for reward computation error_pos = control_action.joint_positions - joint_pos error_vel = control_action.joint_velocities - joint_vel diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/app/app_launcher.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/app/app_launcher.py index 8f9f5adc19..c6dc4b84dc 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/app/app_launcher.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/app/app_launcher.py @@ -185,6 +185,10 @@ def add_app_launcher_args(parser: argparse.ArgumentParser) -> None: * If headless and enable_cameras are False, the experience file is set to ``isaaclab.python.kit``. * If headless is True and enable_cameras is False, the experience file is set to ``isaaclab.python.headless.kit``. + * ``kit_args`` (str): Optional command line arguments to be passed to Omniverse Kit directly. + Arguments should be combined into a single string separated by space. + Example usage: --kit_args "--ext-folder=/path/to/ext1 --ext-folder=/path/to/ext2" + Args: parser: An argument parser instance to be extended with the AppLauncher specific options. """ @@ -254,7 +258,12 @@ def add_app_launcher_args(parser: argparse.ArgumentParser) -> None: arg_group.add_argument( "--verbose", # Note: This is read by SimulationApp through sys.argv action="store_true", - help="Enable verbose terminal output from the SimulationApp.", + help="Enable verbose-level log output from the SimulationApp.", + ) + arg_group.add_argument( + "--info", # Note: This is read by SimulationApp through sys.argv + action="store_true", + help="Enable info-level log output from the SimulationApp.", ) arg_group.add_argument( "--experience", @@ -266,6 +275,15 @@ def add_app_launcher_args(parser: argparse.ArgumentParser) -> None: " it is resolved relative to the `apps` folder in Isaac Sim and Isaac Lab (in that order)." ), ) + arg_group.add_argument( + "--kit_args", + type=str, + default="", + help=( + "Command line arguments for Omniverse Kit as a string separated by a space delimiter." + ' Example usage: --kit_args "--ext-folder=/path/to/ext1 --ext-folder=/path/to/ext2"' + ), + ) # Corresponding to the beginning of the function, # if we have removed -h/--help handling, we add it back. @@ -552,6 +570,14 @@ def _config_resolution(self, launcher_args: dict): " The file does not exist." ) + # Resolve additional arguments passed to Kit + self._kit_args = [] + if "kit_args" in launcher_args: + self._kit_args = [arg for arg in launcher_args["kit_args"].split()] + sys.argv += self._kit_args + + # Resolve the absolute path of the experience file + self._sim_experience_file = os.path.abspath(self._sim_experience_file) print(f"[INFO][AppLauncher]: Loading experience file: {self._sim_experience_file}") # Remove all values from input keyword args which are not meant for SimulationApp # Assign all the passed settings to a dictionary for the simulation app @@ -572,14 +598,25 @@ def _create_app(self): for key in found_modules: hacked_modules[key] = sys.modules[key] del sys.modules[key] + + # disable sys stdout and stderr to avoid printing the warning messages + # this is mainly done to purge the print statements from the simulation app + if "--verbose" not in sys.argv and "--info" not in sys.argv: + sys.stdout = open(os.devnull, "w") # noqa: SIM115 # launch simulation app self._app = SimulationApp(self._sim_app_config, experience=self._sim_experience_file) + # enable sys stdout and stderr + sys.stdout = sys.__stdout__ + # add Isaac Lab modules back to sys.modules for key, value in hacked_modules.items(): sys.modules[key] = value # remove the threadCount argument from sys.argv if it was added for distributed training pattern = r"--/plugins/carb\.tasking\.plugin/threadCount=\d+" sys.argv = [arg for arg in sys.argv if not re.match(pattern, arg)] + # remove additional OV args from sys.argv + if len(self._kit_args) > 0: + sys.argv = [arg for arg in sys.argv if arg not in self._kit_args] def _rendering_enabled(self) -> bool: """Check if rendering is required by the app.""" diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/__init__.py index f0d29abcae..f91e4db7e6 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/__init__.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/__init__.py @@ -43,3 +43,4 @@ from .asset_base_cfg import AssetBaseCfg from .deformable_object import DeformableObject, DeformableObjectCfg, DeformableObjectData from .rigid_object import RigidObject, RigidObjectCfg, RigidObjectData +from .rigid_object_collection import RigidObjectCollection, RigidObjectCollectionCfg, RigidObjectCollectionData diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/articulation/articulation.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/articulation/articulation.py index c1765df56c..ae3634e302 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/articulation/articulation.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/articulation/articulation.py @@ -13,16 +13,16 @@ from prettytable import PrettyTable from typing import TYPE_CHECKING -import carb import omni.isaac.core.utils.stage as stage_utils +import omni.log import omni.physics.tensors.impl.api as physx -from omni.isaac.core.utils.types import ArticulationActions from pxr import PhysxSchema, UsdPhysics import omni.isaac.lab.sim as sim_utils import omni.isaac.lab.utils.math as math_utils import omni.isaac.lab.utils.string as string_utils from omni.isaac.lab.actuators import ActuatorBase, ActuatorBaseCfg, ImplicitActuator +from omni.isaac.lab.utils.types import ArticulationActions from ..asset_base import AssetBase from .articulation_data import ArticulationData @@ -424,6 +424,39 @@ def write_joint_damping_to_sim( # set into simulation self.root_physx_view.set_dof_dampings(self._data.joint_damping.cpu(), indices=physx_env_ids.cpu()) + def write_joint_velocity_limit_to_sim( + self, + limits: torch.Tensor | float, + joint_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Write joint max velocity to the simulation. + + Args: + limits: Joint max velocity. Shape is (len(env_ids), len(joint_ids)). + joint_ids: The joint indices to set the max velocity for. Defaults to None (all joints). + env_ids: The environment indices to set the max velocity for. Defaults to None (all environments). + """ + # resolve indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + if joint_ids is None: + joint_ids = slice(None) + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + # move tensor to cpu if needed + if isinstance(limits, torch.Tensor): + limits = limits.to(self.device) + + # set into internal buffers + self._data.joint_velocity_limits = self.root_physx_view.get_dof_max_velocities().to(self.device) + self._data.joint_velocity_limits[env_ids, joint_ids] = limits + # set into simulation + self.root_physx_view.set_dof_max_velocities(self._data.joint_velocity_limits.cpu(), indices=physx_env_ids.cpu()) + def write_joint_effort_limit_to_sim( self, limits: torch.Tensor | float, @@ -539,6 +572,18 @@ def write_joint_limits_to_sim( env_ids = env_ids[:, None] # set into internal buffers self._data.joint_limits[env_ids, joint_ids] = limits + # update default joint pos to stay within the new limits + if torch.any( + (self._data.default_joint_pos[env_ids, joint_ids] < limits[..., 0]) + | (self._data.default_joint_pos[env_ids, joint_ids] > limits[..., 1]) + ): + self._data.default_joint_pos[env_ids, joint_ids] = torch.clamp( + self._data.default_joint_pos[env_ids, joint_ids], limits[..., 0], limits[..., 1] + ) + omni.log.warn( + "Some default joint positions are outside of the range of the new joint limits. Default joint positions" + " will be clamped to be within the new joint limits." + ) # set into simulation self.root_physx_view.set_dof_limits(self._data.joint_limits.cpu(), indices=physx_env_ids.cpu()) @@ -918,13 +963,13 @@ def _initialize_impl(self): raise RuntimeError(f"Failed to create articulation at: {self.cfg.prim_path}. Please check PhysX logs.") # log information about the articulation - carb.log_info(f"Articulation initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") - carb.log_info(f"Is fixed root: {self.is_fixed_base}") - carb.log_info(f"Number of bodies: {self.num_bodies}") - carb.log_info(f"Body names: {self.body_names}") - carb.log_info(f"Number of joints: {self.num_joints}") - carb.log_info(f"Joint names: {self.joint_names}") - carb.log_info(f"Number of fixed tendons: {self.num_fixed_tendons}") + omni.log.info(f"Articulation initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") + omni.log.info(f"Is fixed root: {self.is_fixed_base}") + omni.log.info(f"Number of bodies: {self.num_bodies}") + omni.log.info(f"Body names: {self.body_names}") + omni.log.info(f"Number of joints: {self.num_joints}") + omni.log.info(f"Joint names: {self.joint_names}") + omni.log.info(f"Number of fixed tendons: {self.num_fixed_tendons}") # container for data access self._data = ArticulationData(self.root_physx_view, self.device) @@ -1098,12 +1143,10 @@ def _process_actuators_cfg(self): self._has_implicit_actuators = False # cache the values coming from the usd - usd_stiffness = self.root_physx_view.get_dof_stiffnesses().clone() - usd_damping = self.root_physx_view.get_dof_dampings().clone() - usd_armature = self.root_physx_view.get_dof_armatures().clone() - usd_friction = self.root_physx_view.get_dof_friction_coefficients().clone() - usd_effort_limit = self.root_physx_view.get_dof_max_forces().clone() - usd_velocity_limit = self.root_physx_view.get_dof_max_velocities().clone() + self._data.default_joint_stiffness = self.root_physx_view.get_dof_stiffnesses().to(self.device).clone() + self._data.default_joint_damping = self.root_physx_view.get_dof_dampings().to(self.device).clone() + self._data.default_joint_armature = self.root_physx_view.get_dof_armatures().to(self.device).clone() + self._data.default_joint_friction = self.root_physx_view.get_dof_friction_coefficients().to(self.device).clone() # iterate over all actuator configurations for actuator_name, actuator_cfg in self.cfg.actuators.items(): @@ -1127,15 +1170,15 @@ def _process_actuators_cfg(self): ), num_envs=self.num_instances, device=self.device, - stiffness=usd_stiffness[:, joint_ids], - damping=usd_damping[:, joint_ids], - armature=usd_armature[:, joint_ids], - friction=usd_friction[:, joint_ids], - effort_limit=usd_effort_limit[:, joint_ids], - velocity_limit=usd_velocity_limit[:, joint_ids], + stiffness=self._data.default_joint_stiffness[:, joint_ids], + damping=self._data.default_joint_damping[:, joint_ids], + armature=self._data.default_joint_armature[:, joint_ids], + friction=self._data.default_joint_friction[:, joint_ids], + effort_limit=self.root_physx_view.get_dof_max_forces().to(self.device).clone()[:, joint_ids], + velocity_limit=self.root_physx_view.get_dof_max_velocities().to(self.device).clone()[:, joint_ids], ) # log information on actuator groups - carb.log_info( + omni.log.info( f"Actuator collection: {actuator_name} with model '{actuator_cfg.class_type.__name__}' and" f" joint names: {joint_names} [{joint_ids}]." ) @@ -1148,6 +1191,7 @@ def _process_actuators_cfg(self): self.write_joint_stiffness_to_sim(actuator.stiffness, joint_ids=actuator.joint_indices) self.write_joint_damping_to_sim(actuator.damping, joint_ids=actuator.joint_indices) self.write_joint_effort_limit_to_sim(actuator.effort_limit, joint_ids=actuator.joint_indices) + self.write_joint_velocity_limit_to_sim(actuator.velocity_limit, joint_ids=actuator.joint_indices) self.write_joint_armature_to_sim(actuator.armature, joint_ids=actuator.joint_indices) self.write_joint_friction_to_sim(actuator.friction, joint_ids=actuator.joint_indices) else: @@ -1156,21 +1200,17 @@ def _process_actuators_cfg(self): self.write_joint_stiffness_to_sim(0.0, joint_ids=actuator.joint_indices) self.write_joint_damping_to_sim(0.0, joint_ids=actuator.joint_indices) self.write_joint_effort_limit_to_sim(1.0e9, joint_ids=actuator.joint_indices) + self.write_joint_velocity_limit_to_sim(actuator.velocity_limit, joint_ids=actuator.joint_indices) self.write_joint_armature_to_sim(actuator.armature, joint_ids=actuator.joint_indices) self.write_joint_friction_to_sim(actuator.friction, joint_ids=actuator.joint_indices) - - # set the default joint parameters based on the changes from the actuators - self._data.default_joint_stiffness = self.root_physx_view.get_dof_stiffnesses().to(device=self.device).clone() - self._data.default_joint_damping = self.root_physx_view.get_dof_dampings().to(device=self.device).clone() - self._data.default_joint_armature = self.root_physx_view.get_dof_armatures().to(device=self.device).clone() - self._data.default_joint_friction = ( - self.root_physx_view.get_dof_friction_coefficients().to(device=self.device).clone() - ) + # Store the actual default stiffness and damping values for explicit actuators (not written the sim) + self._data.default_joint_stiffness[:, actuator.joint_indices] = actuator.stiffness + self._data.default_joint_damping[:, actuator.joint_indices] = actuator.damping # perform some sanity checks to ensure actuators are prepared correctly total_act_joints = sum(actuator.num_joints for actuator in self.actuators.values()) if total_act_joints != (self.num_joints - self.num_fixed_tendons): - carb.log_warn( + omni.log.warn( "Not all actuators are configured! Total number of actuated joints not equal to number of" f" joints available: {total_act_joints} != {self.num_joints - self.num_fixed_tendons}." ) @@ -1183,10 +1223,11 @@ def _process_fixed_tendons(self): # parse fixed tendons properties if they exist if self.num_fixed_tendons > 0: stage = stage_utils.get_current_stage() + joint_paths = self.root_physx_view.dof_paths[0] # iterate over all joints to find tendons attached to them for j in range(self.num_joints): - usd_joint_path = self.root_physx_view.dof_paths[0][j] + usd_joint_path = joint_paths[j] # check whether joint has tendons - tendon name follows the joint name it is attached to joint = UsdPhysics.Joint.Get(stage, usd_joint_path) if joint.GetPrim().HasAPI(PhysxSchema.PhysxTendonAxisRootAPI): @@ -1329,7 +1370,7 @@ def _log_articulation_joint_info(self): effort_limits[index], ]) # convert table to string - carb.log_info(f"Simulation parameters for joints in {self.cfg.prim_path}:\n" + table.get_string()) + omni.log.info(f"Simulation parameters for joints in {self.cfg.prim_path}:\n" + table.get_string()) # read out all tendon parameters from simulation if self.num_fixed_tendons > 0: @@ -1365,4 +1406,4 @@ def _log_articulation_joint_info(self): ft_offsets[index], ]) # convert table to string - carb.log_info(f"Simulation parameters for tendons in {self.cfg.prim_path}:\n" + tendon_table.get_string()) + omni.log.info(f"Simulation parameters for tendons in {self.cfg.prim_path}:\n" + tendon_table.get_string()) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/articulation/articulation_data.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/articulation/articulation_data.py index 4b84f96ba3..633785f2c2 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/articulation/articulation_data.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/articulation/articulation_data.py @@ -220,6 +220,9 @@ def update(self, dt: float): joint_limits: torch.Tensor = None """Joint limits provided to simulation. Shape is (num_instances, num_joints, 2).""" + joint_velocity_limits: torch.Tensor = None + """Joint maximum velocity provided to simulation. Shape is (num_instances, num_joints).""" + ## # Fixed tendon properties. ## 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/assets/deformable_object/deformable_object.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/deformable_object.py index 4a57c316c9..065dd7be18 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/deformable_object.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/deformable_object.py @@ -9,7 +9,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -import carb +import omni.log import omni.physics.tensors.impl.api as physx from pxr import PhysxSchema, UsdShade @@ -305,7 +305,7 @@ def _initialize_impl(self): material_prim = mat_prim break if material_prim is None: - carb.log_info( + omni.log.info( f"Failed to find a deformable material binding for '{root_prim.GetPath().pathString}'." " The material properties will be set to default values and are not modifiable at runtime." " If you want to modify the material properties, please ensure that the material is bound" @@ -341,14 +341,14 @@ def _initialize_impl(self): self._material_physx_view = None # log information about the deformable body - carb.log_info(f"Deformable body initialized at: {root_prim_path_expr}") - carb.log_info(f"Number of instances: {self.num_instances}") - carb.log_info(f"Number of bodies: {self.num_bodies}") + omni.log.info(f"Deformable body initialized at: {root_prim_path_expr}") + omni.log.info(f"Number of instances: {self.num_instances}") + omni.log.info(f"Number of bodies: {self.num_bodies}") if self._material_physx_view is not None: - carb.log_info(f"Deformable material initialized at: {material_prim_path_expr}") - carb.log_info(f"Number of instances: {self._material_physx_view.count}") + omni.log.info(f"Deformable material initialized at: {material_prim_path_expr}") + omni.log.info(f"Number of instances: {self._material_physx_view.count}") else: - carb.log_info("No deformable material found. Material properties will be set to default values.") + omni.log.info("No deformable material found. Material properties will be set to default values.") # container for data access self._data = DeformableObjectData(self.root_physx_view, self.device) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object/rigid_object.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object/rigid_object.py index 328a3e76c6..4908db4dc5 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object/rigid_object.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object/rigid_object.py @@ -9,7 +9,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -import carb +import omni.log import omni.physics.tensors.impl.api as physx from pxr import UsdPhysics @@ -257,6 +257,9 @@ def set_external_force_and_torque( self._external_torque_b[env_ids, body_ids] = torques else: self.has_external_wrench = False + # reset external wrench + self._external_force_b[env_ids] = 0.0 + self._external_torque_b[env_ids] = 0.0 """ Internal helper. @@ -288,6 +291,18 @@ def _initialize_impl(self): " Please ensure that there is only one rigid body in the prim path tree." ) + articulation_prims = sim_utils.get_all_matching_child_prims( + template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI) + ) + if len(articulation_prims) != 0: + if articulation_prims[0].GetAttribute("physxArticulation:articulationEnabled").Get(): + raise RuntimeError( + f"Found an articulation root when resolving '{self.cfg.prim_path}' for rigid objects. These are" + f" located at: '{articulation_prims}' under '{template_prim_path}'. Please disable the articulation" + " root in the USD or from code by setting the parameter" + " 'ArticulationRootPropertiesCfg.articulation_enabled' to False in the spawn configuration." + ) + # resolve root prim back into regex expression root_prim_path = root_prims[0].GetPath().pathString root_prim_path_expr = self.cfg.prim_path + root_prim_path[len(template_prim_path) :] @@ -299,10 +314,10 @@ def _initialize_impl(self): raise RuntimeError(f"Failed to create rigid body at: {self.cfg.prim_path}. Please check PhysX logs.") # log information about the rigid body - carb.log_info(f"Rigid body initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") - carb.log_info(f"Number of instances: {self.num_instances}") - carb.log_info(f"Number of bodies: {self.num_bodies}") - carb.log_info(f"Body names: {self.body_names}") + omni.log.info(f"Rigid body initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") + omni.log.info(f"Number of instances: {self.num_instances}") + omni.log.info(f"Number of bodies: {self.num_bodies}") + omni.log.info(f"Body names: {self.body_names}") # container for data access self._data = RigidObjectData(self.root_physx_view, self.device) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object_collection/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object_collection/__init__.py new file mode 100644 index 0000000000..c0f4b5a46c --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object_collection/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Sub-module for rigid object collection.""" + +from .rigid_object_collection import RigidObjectCollection +from .rigid_object_collection_cfg import RigidObjectCollectionCfg +from .rigid_object_collection_data import RigidObjectCollectionData diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection.py new file mode 100644 index 0000000000..53fca921e7 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection.py @@ -0,0 +1,507 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import re +import torch +import weakref +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import omni.kit.app +import omni.log +import omni.physics.tensors.impl.api as physx +import omni.timeline +from pxr import UsdPhysics + +import omni.isaac.lab.sim as sim_utils +import omni.isaac.lab.utils.math as math_utils +import omni.isaac.lab.utils.string as string_utils + +from ..asset_base import AssetBase +from .rigid_object_collection_data import RigidObjectCollectionData + +if TYPE_CHECKING: + from .rigid_object_collection_cfg import RigidObjectCollectionCfg + + +class RigidObjectCollection(AssetBase): + """A rigid object collection class. + + This class represents a collection of rigid objects in the simulation, where the state of the rigid objects can be + accessed and modified using a batched ``(env_ids, object_ids)`` API. + + For each rigid body in the collection, the root prim of the asset must have the `USD RigidBodyAPI`_ + applied to it. This API is used to define the simulation properties of the rigid bodies. On playing the + simulation, the physics engine will automatically register the rigid bodies and create a corresponding + rigid body handle. This handle can be accessed using the :attr:`root_physx_view` attribute. + + .. note:: + Rigid objects in the collection are uniquely identified via the key of the dictionary + :attr:`~omni.isaac.lab.assets.RigidObjectCollectionCfg.rigid_objects` in :class:`~omni.isaac.lab.assets.RigidObjectCollectionCfg`. + This differs from the class :class:`~omni.isaac.lab.assets.RigidObject`, where a rigid object is identified by + the name of the Xform where the `USD RigidBodyAPI`_ is applied. This would not be possible for the rigid object + collection since the :attr:`~omni.isaac.lab.assets.RigidObjectCollectionCfg.rigid_objects` dictionary could + contain the same rigid object multiple times, leading to ambiguity. + + .. _`USD RigidBodyAPI`: https://openusd.org/dev/api/class_usd_physics_rigid_body_a_p_i.html + """ + + cfg: RigidObjectCollectionCfg + """Configuration instance for the rigid object collection.""" + + def __init__(self, cfg: RigidObjectCollectionCfg): + """Initialize the rigid object collection. + + Args: + cfg: A configuration instance. + """ + # check that the config is valid + cfg.validate() + # store inputs + self.cfg = cfg + # flag for whether the asset is initialized + self._is_initialized = False + for rigid_object_cfg in self.cfg.rigid_objects.values(): + # check if the rigid object path is valid + # note: currently the spawner does not work if there is a regex pattern in the leaf + # For example, if the prim path is "/World/Object_[1,2]" since the spawner will not + # know which prim to spawn. This is a limitation of the spawner and not the asset. + asset_path = rigid_object_cfg.prim_path.split("/")[-1] + asset_path_is_regex = re.match(r"^[a-zA-Z0-9/_]+$", asset_path) is None + # spawn the asset + if rigid_object_cfg.spawn is not None and not asset_path_is_regex: + rigid_object_cfg.spawn.func( + rigid_object_cfg.prim_path, + rigid_object_cfg.spawn, + translation=rigid_object_cfg.init_state.pos, + orientation=rigid_object_cfg.init_state.rot, + ) + # check that spawn was successful + matching_prims = sim_utils.find_matching_prims(rigid_object_cfg.prim_path) + if len(matching_prims) == 0: + raise RuntimeError(f"Could not find prim with path {rigid_object_cfg.prim_path}.") + + # stores object names + self._object_names_list = [] + + # note: Use weakref on all callbacks to ensure that this object can be deleted when its destructor is called. + # add callbacks for stage play/stop + # The order is set to 10 which is arbitrary but should be lower priority than the default order of 0 + timeline_event_stream = omni.timeline.get_timeline_interface().get_timeline_event_stream() + self._initialize_handle = timeline_event_stream.create_subscription_to_pop_by_type( + int(omni.timeline.TimelineEventType.PLAY), + lambda event, obj=weakref.proxy(self): obj._initialize_callback(event), + order=10, + ) + self._invalidate_initialize_handle = timeline_event_stream.create_subscription_to_pop_by_type( + int(omni.timeline.TimelineEventType.STOP), + lambda event, obj=weakref.proxy(self): obj._invalidate_initialize_callback(event), + order=10, + ) + + self._debug_vis_handle = None + + """ + Properties + """ + + @property + def data(self) -> RigidObjectCollectionData: + return self._data + + @property + def num_instances(self) -> int: + """Number of instances of the collection.""" + return self.root_physx_view.count // self.num_objects + + @property + def num_objects(self) -> int: + """Number of objects in the collection. + + This corresponds to the distinct number of rigid bodies in the collection. + """ + return len(self.object_names) + + @property + def object_names(self) -> list[str]: + """Ordered names of objects in the rigid object collection.""" + return self._object_names_list + + @property + def root_physx_view(self) -> physx.RigidBodyView: + """Rigid body view for the rigid body collection (PhysX). + + Note: + Use this view with caution. It requires handling of tensors in a specific way. + """ + return self._root_physx_view + + """ + Operations. + """ + + def reset(self, env_ids: torch.Tensor | None = None, object_ids: slice | torch.Tensor | None = None): + """Resets all internal buffers of selected environments and objects. + + Args: + env_ids: The indices of the object to reset. Defaults to None (all instances). + object_ids: The indices of the object to reset. Defaults to None (all objects). + """ + # resolve all indices + if env_ids is None: + env_ids = self._ALL_ENV_INDICES + if object_ids is None: + object_ids = self._ALL_OBJ_INDICES + # reset external wrench + self._external_force_b[env_ids[:, None], object_ids] = 0.0 + self._external_torque_b[env_ids[:, None], object_ids] = 0.0 + + def write_data_to_sim(self): + """Write external wrench to the simulation. + + Note: + We write external wrench to the simulation here since this function is called before the simulation step. + This ensures that the external wrench is applied at every simulation step. + """ + # write external wrench + if self.has_external_wrench: + self.root_physx_view.apply_forces_and_torques_at_position( + force_data=self.reshape_data_to_view(self._external_force_b), + torque_data=self.reshape_data_to_view(self._external_torque_b), + position_data=None, + indices=self._env_obj_ids_to_view_ids(self._ALL_ENV_INDICES, self._ALL_OBJ_INDICES), + is_global=False, + ) + + def update(self, dt: float): + self._data.update(dt) + + """ + Operations - Finders. + """ + + def find_objects( + self, name_keys: str | Sequence[str], preserve_order: bool = False + ) -> tuple[torch.Tensor, list[str]]: + """Find objects in the collection based on the name keys. + + Please check the :meth:`omni.isaac.lab.utils.string_utils.resolve_matching_names` function for more + information on the name matching. + + Args: + name_keys: A regular expression or a list of regular expressions to match the object names. + preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False. + + Returns: + A tuple containing the object indices and names. + """ + obj_ids, obj_names = string_utils.resolve_matching_names(name_keys, self.object_names, preserve_order) + return torch.tensor(obj_ids, device=self.device), obj_names + + """ + Operations - Write to simulation. + """ + + def write_object_state_to_sim( + self, + object_state: torch.Tensor, + env_ids: torch.Tensor | None = None, + object_ids: slice | torch.Tensor | None = None, + ): + """Set the object state over selected environment and object indices into the simulation. + + The object state comprises of the cartesian position, quaternion orientation in (w, x, y, z), and linear + and angular velocity. All the quantities are in the simulation frame. + + Args: + object_state: Object state in simulation frame. Shape is (len(env_ids), len(object_ids), 13). + env_ids: Environment indices. If None, then all indices are used. + object_ids: Object indices. If None, then all indices are used. + """ + # set into simulation + self.write_object_pose_to_sim(object_state[..., :7], env_ids=env_ids, object_ids=object_ids) + self.write_object_velocity_to_sim(object_state[..., 7:], env_ids=env_ids, object_ids=object_ids) + + def write_object_pose_to_sim( + self, + object_pose: torch.Tensor, + env_ids: torch.Tensor | None = None, + object_ids: slice | torch.Tensor | None = None, + ): + """Set the object pose over selected environment and object indices into the simulation. + + The object pose comprises of the cartesian position and quaternion orientation in (w, x, y, z). + + Args: + object_pose: Object poses in simulation frame. Shape is (len(env_ids), len(object_ids), 7). + env_ids: Environment indices. If None, then all indices are used. + object_ids: Object indices. If None, then all indices are used. + """ + # resolve all indices + # -- env_ids + if env_ids is None: + env_ids = self._ALL_ENV_INDICES + # -- object_ids + if object_ids is None: + object_ids = self._ALL_OBJ_INDICES + # note: we need to do this here since tensors are not set into simulation until step. + # set into internal buffers + self._data.object_state_w[env_ids[:, None], object_ids, :7] = object_pose.clone() + # convert the quaternion from wxyz to xyzw + poses_xyzw = self._data.object_state_w[..., :7].clone() + poses_xyzw[..., 3:] = math_utils.convert_quat(poses_xyzw[..., 3:], to="xyzw") + # set into simulation + view_ids = self._env_obj_ids_to_view_ids(env_ids, object_ids) + self.root_physx_view.set_transforms(self.reshape_data_to_view(poses_xyzw), indices=view_ids) + + def write_object_velocity_to_sim( + self, + object_velocity: torch.Tensor, + env_ids: torch.Tensor | None = None, + object_ids: slice | torch.Tensor | None = None, + ): + """Set the object velocity over selected environment and object indices into the simulation. + + Args: + object_velocity: Object velocities in simulation frame. Shape is (len(env_ids), len(object_ids), 6). + env_ids: Environment indices. If None, then all indices are used. + object_ids: Object indices. If None, then all indices are used. + """ + # resolve all indices + # -- env_ids + if env_ids is None: + env_ids = self._ALL_ENV_INDICES + # -- object_ids + if object_ids is None: + object_ids = self._ALL_OBJ_INDICES + + self._data.object_state_w[env_ids[:, None], object_ids, 7:] = object_velocity.clone() + self._data.object_acc_w[env_ids[:, None], object_ids] = 0.0 + + # set into simulation + view_ids = self._env_obj_ids_to_view_ids(env_ids, object_ids) + self.root_physx_view.set_velocities( + self.reshape_data_to_view(self._data.object_state_w[..., 7:]), indices=view_ids + ) + + """ + Operations - Setters. + """ + + def set_external_force_and_torque( + self, + forces: torch.Tensor, + torques: torch.Tensor, + object_ids: slice | torch.Tensor | None = None, + env_ids: torch.Tensor | None = None, + ): + """Set external force and torque to apply on the objects' bodies in their local frame. + + For many applications, we want to keep the applied external force on rigid bodies constant over a period of + time (for instance, during the policy control). This function allows us to store the external force and torque + into buffers which are then applied to the simulation at every step. + + .. caution:: + If the function is called with empty forces and torques, then this function disables the application + of external wrench to the simulation. + + .. code-block:: python + + # example of disabling external wrench + asset.set_external_force_and_torque(forces=torch.zeros(0, 0, 3), torques=torch.zeros(0, 0, 3)) + + .. note:: + This function does not apply the external wrench to the simulation. It only fills the buffers with + the desired values. To apply the external wrench, call the :meth:`write_data_to_sim` function + right before the simulation step. + + Args: + forces: External forces in bodies' local frame. Shape is (len(env_ids), len(object_ids), 3). + torques: External torques in bodies' local frame. Shape is (len(env_ids), len(object_ids), 3). + object_ids: Object indices to apply external wrench to. Defaults to None (all objects). + env_ids: Environment indices to apply external wrench to. Defaults to None (all instances). + """ + if forces.any() or torques.any(): + self.has_external_wrench = True + # resolve all indices + # -- env_ids + if env_ids is None: + env_ids = self._ALL_ENV_INDICES + # -- object_ids + if object_ids is None: + object_ids = self._ALL_OBJ_INDICES + # set into internal buffers + self._external_force_b[env_ids[:, None], object_ids] = forces + self._external_torque_b[env_ids[:, None], object_ids] = torques + else: + self.has_external_wrench = False + + """ + Internal helper. + """ + + def _initialize_impl(self): + # create simulation view + self._physics_sim_view = physx.create_simulation_view(self._backend) + self._physics_sim_view.set_subspace_roots("/") + root_prim_path_exprs = [] + for name, rigid_object_cfg in self.cfg.rigid_objects.items(): + # obtain the first prim in the regex expression (all others are assumed to be a copy of this) + template_prim = sim_utils.find_first_matching_prim(rigid_object_cfg.prim_path) + if template_prim is None: + raise RuntimeError(f"Failed to find prim for expression: '{rigid_object_cfg.prim_path}'.") + template_prim_path = template_prim.GetPath().pathString + + # find rigid root prims + root_prims = sim_utils.get_all_matching_child_prims( + template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.RigidBodyAPI) + ) + if len(root_prims) == 0: + raise RuntimeError( + f"Failed to find a rigid body when resolving '{rigid_object_cfg.prim_path}'." + " Please ensure that the prim has 'USD RigidBodyAPI' applied." + ) + if len(root_prims) > 1: + raise RuntimeError( + f"Failed to find a single rigid body when resolving '{rigid_object_cfg.prim_path}'." + f" Found multiple '{root_prims}' under '{template_prim_path}'." + " Please ensure that there is only one rigid body in the prim path tree." + ) + + # check that no rigid object has an articulation root API, which decreases simulation performance + articulation_prims = sim_utils.get_all_matching_child_prims( + template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI) + ) + if len(articulation_prims) != 0: + if articulation_prims[0].GetAttribute("physxArticulation:articulationEnabled").Get(): + raise RuntimeError( + f"Found an articulation root when resolving '{rigid_object_cfg.prim_path}' in the rigid object" + f" collection. These are located at: '{articulation_prims}' under '{template_prim_path}'." + " Please disable the articulation root in the USD or from code by setting the parameter" + " 'ArticulationRootPropertiesCfg.articulation_enabled' to False in the spawn configuration." + ) + + # resolve root prim back into regex expression + root_prim_path = root_prims[0].GetPath().pathString + root_prim_path_expr = rigid_object_cfg.prim_path + root_prim_path[len(template_prim_path) :] + root_prim_path_exprs.append(root_prim_path_expr.replace(".*", "*")) + + self._object_names_list.append(name) + + # -- object view + self._root_physx_view = self._physics_sim_view.create_rigid_body_view(root_prim_path_exprs) + + # check if the rigid body was created + if self._root_physx_view._backend is None: + raise RuntimeError("Failed to create rigid body collection. Please check PhysX logs.") + + # log information about the rigid body + omni.log.info(f"Number of instances: {self.num_instances}") + omni.log.info(f"Number of distinct objects: {self.num_objects}") + omni.log.info(f"Object names: {self.object_names}") + + # container for data access + self._data = RigidObjectCollectionData(self.root_physx_view, self.num_objects, self.device) + + # create buffers + self._create_buffers() + # process configuration + self._process_cfg() + # update the rigid body data + self.update(0.0) + + def _create_buffers(self): + """Create buffers for storing data.""" + # constants + self._ALL_ENV_INDICES = torch.arange(self.num_instances, dtype=torch.long, device=self.device) + self._ALL_OBJ_INDICES = torch.arange(self.num_objects, dtype=torch.long, device=self.device) + + # external forces and torques + self.has_external_wrench = False + self._external_force_b = torch.zeros((self.num_instances, self.num_objects, 3), device=self.device) + self._external_torque_b = torch.zeros_like(self._external_force_b) + + # set information about rigid body into data + self._data.object_names = self.object_names + self._data.default_mass = self.reshape_view_to_data(self.root_physx_view.get_masses().clone()) + self._data.default_inertia = self.reshape_view_to_data(self.root_physx_view.get_inertias().clone()) + + def _process_cfg(self): + """Post processing of configuration parameters.""" + # default state + # -- object state + default_object_states = [] + for rigid_object_cfg in self.cfg.rigid_objects.values(): + default_object_state = ( + tuple(rigid_object_cfg.init_state.pos) + + tuple(rigid_object_cfg.init_state.rot) + + tuple(rigid_object_cfg.init_state.lin_vel) + + tuple(rigid_object_cfg.init_state.ang_vel) + ) + default_object_state = ( + torch.tensor(default_object_state, dtype=torch.float, device=self.device) + .repeat(self.num_instances, 1) + .unsqueeze(1) + ) + default_object_states.append(default_object_state) + # concatenate the default state for each object + default_object_states = torch.cat(default_object_states, dim=1) + self._data.default_object_state = default_object_states + + def reshape_view_to_data(self, data: torch.Tensor) -> torch.Tensor: + """Reshapes and arranges the data coming from the :attr:`root_physx_view` to (num_instances, num_objects, data_size). + + Args: + data: The data coming from the :attr:`root_physx_view`. Shape is (num_instances*num_objects, data_size). + + Returns: + The reshaped data. Shape is (num_instances, num_objects, data_size). + """ + return torch.einsum("ijk -> jik", data.reshape(self.num_objects, self.num_instances, -1)) + + def reshape_data_to_view(self, data: torch.Tensor) -> torch.Tensor: + """Reshapes and arranges the data to the be consistent with data from the :attr:`root_physx_view`. + + Args: + data: The data to be reshaped. Shape is (num_instances, num_objects, data_size). + + Returns: + The reshaped data. Shape is (num_instances*num_objects, data_size). + """ + return torch.einsum("ijk -> jik", data).reshape(self.num_objects * self.num_instances, *data.shape[2:]) + + def _env_obj_ids_to_view_ids( + self, env_ids: torch.Tensor, object_ids: Sequence[int] | slice | torch.Tensor + ) -> torch.Tensor: + """Converts environment and object indices to indices consistent with data from :attr:`root_physx_view`. + + Args: + env_ids: Environment indices. + object_ids: Object indices. + + Returns: + The view indices. + """ + # the order is env_0/object_0, env_0/object_1, env_0/object_..., env_1/object_0, env_1/object_1, ... + # return a flat tensor of indices + if isinstance(object_ids, slice): + object_ids = self._ALL_OBJ_INDICES + elif isinstance(object_ids, Sequence): + object_ids = torch.tensor(object_ids, device=self.device) + return (object_ids.unsqueeze(1) * self.num_instances + env_ids).flatten() + + """ + Internal simulation callbacks. + """ + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + # call parent + super()._invalidate_initialize_callback(event) + # set all existing views to None to invalidate them + self._physics_sim_view = None + self._root_physx_view = None diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection_cfg.py new file mode 100644 index 0000000000..bf21fd6091 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection_cfg.py @@ -0,0 +1,28 @@ +# 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.assets.rigid_object import RigidObjectCfg +from omni.isaac.lab.utils import configclass + +from .rigid_object_collection import RigidObjectCollection + + +@configclass +class RigidObjectCollectionCfg: + """Configuration parameters for a rigid object collection.""" + + class_type: type = RigidObjectCollection + """The associated asset class. + + The class should inherit from :class:`omni.isaac.lab.assets.asset_base.AssetBase`. + """ + + rigid_objects: dict[str, RigidObjectCfg] = MISSING + """Dictionary of rigid object configurations to spawn. + + The keys are the names for the objects, which are used as unique identifiers throughout the code. + """ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection_data.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection_data.py new file mode 100644 index 0000000000..c9930cf740 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection_data.py @@ -0,0 +1,251 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import torch +import weakref + +import omni.physics.tensors.impl.api as physx + +import omni.isaac.lab.utils.math as math_utils +from omni.isaac.lab.utils.buffers import TimestampedBuffer + + +class RigidObjectCollectionData: + """Data container for a rigid object collection. + + This class contains the data for a rigid object collection in the simulation. The data includes the state of + all the bodies in the collection. The data is stored in the simulation world frame unless otherwise specified. + The data is in the order ``(num_instances, num_objects, data_size)``, where data_size is the size of the data. + + For a rigid body, there are two frames of reference that are used: + + - Actor frame: The frame of reference of the rigid body prim. This typically corresponds to the Xform prim + with the rigid body schema. + - Center of mass frame: The frame of reference of the center of mass of the rigid body. + + Depending on the settings of the simulation, the actor frame and the center of mass frame may be the same. + This needs to be taken into account when interpreting the data. + + The data is lazily updated, meaning that the data is only updated when it is accessed. This is useful + when the data is expensive to compute or retrieve. The data is updated when the timestamp of the buffer + is older than the current simulation timestamp. The timestamp is updated whenever the data is updated. + """ + + def __init__(self, root_physx_view: physx.RigidBodyView, num_objects: int, device: str): + """Initializes the data. + + Args: + root_physx_view: The root rigid body view. + num_objects: The number of objects in the collection. + device: The device used for processing. + """ + # Set the parameters + self.device = device + self.num_objects = num_objects + # Set the root rigid body view + # note: this is stored as a weak reference to avoid circular references between the asset class + # and the data container. This is important to avoid memory leaks. + self._root_physx_view: physx.RigidBodyView = weakref.proxy(root_physx_view) + self.num_instances = self._root_physx_view.count // self.num_objects + + # Set initial time stamp + self._sim_timestamp = 0.0 + + # Obtain global physics sim view + physics_sim_view = physx.create_simulation_view("torch") + physics_sim_view.set_subspace_roots("/") + gravity = physics_sim_view.get_gravity() + # Convert to direction vector + gravity_dir = torch.tensor((gravity[0], gravity[1], gravity[2]), device=self.device) + gravity_dir = math_utils.normalize(gravity_dir.unsqueeze(0)).squeeze(0) + + # Initialize constants + self.GRAVITY_VEC_W = gravity_dir.repeat(self.num_instances, self.num_objects, 1) + self.FORWARD_VEC_B = torch.tensor((1.0, 0.0, 0.0), device=self.device).repeat( + self.num_instances, self.num_objects, 1 + ) + + # Initialize the lazy buffers. + self._object_state_w = TimestampedBuffer() + self._object_acc_w = TimestampedBuffer() + + def update(self, dt: float): + """Updates the data for the rigid object collection. + + Args: + dt: The time step for the update. This must be a positive value. + """ + # update the simulation timestamp + self._sim_timestamp += dt + + ## + # Names. + ## + + object_names: list[str] = None + """Object names in the order parsed by the simulation view.""" + + ## + # Defaults. + ## + + default_object_state: torch.Tensor = None + """Default object state ``[pos, quat, lin_vel, ang_vel]`` in local environment frame. Shape is (num_instances, num_objects, 13). + + The position and quaternion are of each object's rigid body's actor frame. Meanwhile, the linear and angular velocities are + of the center of mass frame. + """ + + default_mass: torch.Tensor = None + """Default object mass read from the simulation. Shape is (num_instances, num_objects, 1).""" + + default_inertia: torch.Tensor = None + """Default object inertia tensor read from the simulation. Shape is (num_instances, num_objects, 9). + + The inertia is the inertia tensor relative to the center of mass frame. The values are stored in + the order :math:`[I_{xx}, I_{xy}, I_{xz}, I_{yx}, I_{yy}, I_{yz}, I_{zx}, I_{zy}, I_{zz}]`. + """ + + ## + # Properties. + ## + + @property + def object_state_w(self): + """Object state ``[pos, quat, lin_vel, ang_vel]`` in simulation world frame. Shape is (num_instances, num_objects, 13). + + The position and orientation are of the rigid body's actor frame. Meanwhile, the linear and angular + velocities are of the rigid body's center of mass frame. + """ + if self._object_state_w.timestamp < self._sim_timestamp: + # read data from simulation + pose = self._reshape_view_to_data(self._root_physx_view.get_transforms().clone()) + pose[..., 3:7] = math_utils.convert_quat(pose[..., 3:7], to="wxyz") + velocity = self._reshape_view_to_data(self._root_physx_view.get_velocities()) + # set the buffer data and timestamp + self._object_state_w.data = torch.cat((pose, velocity), dim=-1) + self._object_state_w.timestamp = self._sim_timestamp + return self._object_state_w.data + + @property + def object_acc_w(self): + """Acceleration of all objects. Shape is (num_instances, num_objects, 6). + + This quantity is the acceleration of the rigid bodies' center of mass frame. + """ + if self._object_acc_w.timestamp < self._sim_timestamp: + # note: we use finite differencing to compute acceleration + self._object_acc_w.data = self._reshape_view_to_data(self._root_physx_view.get_accelerations().clone()) + self._object_acc_w.timestamp = self._sim_timestamp + return self._object_acc_w.data + + @property + def projected_gravity_b(self): + """Projection of the gravity direction on base frame. Shape is (num_instances, num_objects, 3).""" + return math_utils.quat_rotate_inverse(self.object_quat_w, self.GRAVITY_VEC_W) + + @property + def heading_w(self): + """Yaw heading of the base frame (in radians). Shape is (num_instances, num_objects,). + + Note: + This quantity is computed by assuming that the forward-direction of the base + frame is along x-direction, i.e. :math:`(1, 0, 0)`. + """ + forward_w = math_utils.quat_apply(self.object_quat_w, self.FORWARD_VEC_B) + return torch.atan2(forward_w[..., 1], forward_w[..., 0]) + + ## + # Derived properties. + ## + + @property + def object_pos_w(self) -> torch.Tensor: + """Object position in simulation world frame. Shape is (num_instances, num_objects, 3). + + This quantity is the position of the actor frame of the rigid bodies. + """ + return self.object_state_w[..., :3] + + @property + def object_quat_w(self) -> torch.Tensor: + """Object orientation (w, x, y, z) in simulation world frame. Shape is (num_instances, num_objects, 4). + + This quantity is the orientation of the actor frame of the rigid bodies. + """ + return self.object_state_w[..., 3:7] + + @property + def object_vel_w(self) -> torch.Tensor: + """Object velocity in simulation world frame. Shape is (num_instances, num_objects, 6). + + This quantity contains the linear and angular velocities of the rigid bodies' center of mass frame. + """ + return self.object_state_w[..., 7:13] + + @property + def object_lin_vel_w(self) -> torch.Tensor: + """Object linear velocity in simulation world frame. Shape is (num_instances, num_objects, 3). + + This quantity is the linear velocity of the rigid bodies' center of mass frame. + """ + return self.object_state_w[..., 7:10] + + @property + def object_ang_vel_w(self) -> torch.Tensor: + """Object angular velocity in simulation world frame. Shape is (num_instances, num_objects, 3). + + This quantity is the angular velocity of the rigid bodies' center of mass frame. + """ + return self.object_state_w[..., 10:13] + + @property + def object_lin_vel_b(self) -> torch.Tensor: + """Object linear velocity in base frame. Shape is (num_instances, num_objects, 3). + + This quantity is the linear velocity of the rigid bodies' center of mass frame with respect to the + rigid body's actor frame. + """ + return math_utils.quat_rotate_inverse(self.object_quat_w, self.object_lin_vel_w) + + @property + def object_ang_vel_b(self) -> torch.Tensor: + """Object angular velocity in base world frame. Shape is (num_instances, num_objects, 3). + + This quantity is the angular velocity of the rigid bodies' center of mass frame with respect to the + rigid body's actor frame. + """ + return math_utils.quat_rotate_inverse(self.object_quat_w, self.object_ang_vel_w) + + @property + def object_lin_acc_w(self) -> torch.Tensor: + """Linear acceleration of all bodies in simulation world frame. Shape is (num_instances, num_objects, 3). + + This quantity is the linear acceleration of the rigid bodies' center of mass frame. + """ + return self.object_acc_w[..., 0:3] + + @property + def object_ang_acc_w(self) -> torch.Tensor: + """Angular acceleration of all bodies in simulation world frame. Shape is (num_instances, num_objects, 3). + + This quantity is the angular acceleration of the rigid bodies' center of mass frame. + """ + return self.object_acc_w[..., 3:6] + + ## + # Helpers. + ## + + def _reshape_view_to_data(self, data: torch.Tensor) -> torch.Tensor: + """Reshapes and arranges the data from the physics view to (num_instances, num_objects, data_size). + + Args: + data: The data from the physics view. Shape is (num_instances*num_objects, data_size). + + Returns: + The reshaped data. Shape is (num_objects, num_instances, data_size). + """ + return torch.einsum("ijk -> jik", data.reshape(self.num_objects, self.num_instances, -1)) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/__init__.py index 97701e50cc..667c0b5049 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/__init__.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/__init__.py @@ -52,4 +52,4 @@ from .manager_based_env_cfg import ManagerBasedEnvCfg from .manager_based_rl_env import ManagerBasedRLEnv from .manager_based_rl_env_cfg import ManagerBasedRLEnvCfg -from .utils import multi_agent_to_single_agent, multi_agent_with_one_agent +from .utils.marl import multi_agent_to_single_agent, multi_agent_with_one_agent diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/common.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/common.py index d0c99f8ad8..7d6b02d309 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/common.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/common.py @@ -5,6 +5,7 @@ from __future__ import annotations +import gymnasium as gym import torch from typing import Dict, Literal, TypeVar @@ -62,6 +63,9 @@ class ViewerCfg: # Types. ## +SpaceType = TypeVar("SpaceType", gym.spaces.Space, int, set, tuple, list, dict) +"""A sentinel object to indicate a valid space type to specify states, observations and actions.""" + VecEnvObs = Dict[str, torch.Tensor | Dict[str, torch.Tensor]] """Observation returned by the environment. 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 ab7cb5e3c0..55b8a9501d 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 @@ -14,11 +14,12 @@ import weakref from abc import abstractmethod from collections.abc import Sequence +from dataclasses import MISSING from typing import Any, ClassVar -import carb import omni.isaac.core.utils.torch as torch_utils import omni.kit.app +import omni.log from omni.isaac.version import get_version from omni.isaac.lab.managers import EventManager @@ -30,9 +31,10 @@ from .common import ActionType, AgentID, EnvStepReturn, ObsType, StateType from .direct_marl_env_cfg import DirectMARLEnvCfg from .ui import ViewportCameraController +from .utils.spaces import sample_space, spec_to_gym_space -class DirectMARLEnv: +class DirectMARLEnv(gym.Env): """The superclass for the direct workflow to design multi-agent environments. This class implements the core functionality for multi-agent reinforcement learning (MARL) @@ -72,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 @@ -83,7 +87,7 @@ def __init__(self, cfg: DirectMARLEnvCfg, render_mode: str | None = None, **kwar if self.cfg.seed is not None: self.cfg.seed = self.seed(self.cfg.seed) else: - carb.log_warn("Seed not set for the environment. The environment creation may not be deterministic.") + omni.log.warn("Seed not set for the environment. The environment creation may not be deterministic.") # create a simulation context to control the simulator if SimulationContext.instance() is None: @@ -102,10 +106,10 @@ def __init__(self, cfg: DirectMARLEnvCfg, render_mode: str | None = None, **kwar if self.cfg.sim.render_interval < self.cfg.decimation: msg = ( f"The render interval ({self.cfg.sim.render_interval}) is smaller than the decimation " - f"({self.cfg.decimation}). Multiple multiple render calls will happen for each environment step." + f"({self.cfg.decimation}). Multiple render calls will happen for each environment step." "If this is not intended, set the render interval to be equal to the decimation." ) - carb.log_warn(msg) + omni.log.warn(msg) # generate scene with Timer("[INFO]: Time taken for scene creation", "scene_creation"): @@ -164,10 +168,6 @@ def __init__(self, cfg: DirectMARLEnvCfg, render_mode: str | None = None, **kwar # -- init buffers self.episode_length_buf = torch.zeros(self.num_envs, device=self.device, dtype=torch.long) self.reset_buf = torch.zeros(self.num_envs, dtype=torch.bool, device=self.sim.device) - self.actions = { - agent: torch.zeros(self.num_envs, self.cfg.num_actions[agent], device=self.sim.device) - for agent in self.cfg.possible_agents - } # setup the observation, state and action spaces self._configure_env_spaces() @@ -175,13 +175,13 @@ def __init__(self, cfg: DirectMARLEnvCfg, render_mode: str | None = None, **kwar # setup noise cfg for adding action and observation noise if self.cfg.action_noise_model: self._action_noise_model: dict[AgentID, NoiseModel] = { - agent: noise_model.class_type(self.num_envs, noise_model, self.device) + agent: noise_model.class_type(noise_model, num_envs=self.num_envs, device=self.device) for agent, noise_model in self.cfg.action_noise_model.items() if noise_model is not None } if self.cfg.observation_noise_model: self._observation_noise_model: dict[AgentID, NoiseModel] = { - agent: noise_model.class_type(self.num_envs, noise_model, self.device) + agent: noise_model.class_type(noise_model, num_envs=self.num_envs, device=self.device) for agent, noise_model in self.cfg.observation_noise_model.items() if noise_model is not None } @@ -406,16 +406,19 @@ def state(self) -> StateType | None: """Returns the state for the environment. The state-space is used for centralized training or asymmetric actor-critic architectures. It is configured - using the :attr:`DirectMARLEnvCfg.num_states` parameter. + using the :attr:`DirectMARLEnvCfg.state_space` parameter. Returns: - The states for the environment, or None if :attr:`DirectMARLEnvCfg.num_states` parameter is zero. + The states for the environment, or None if :attr:`DirectMARLEnvCfg.state_space` parameter is zero. """ - if not self.cfg.num_states: + if not self.cfg.state_space: return None # concatenate and return the observations as state - if self.cfg.num_states < 0: - self.state_buf = torch.cat([self.obs_dict[agent] for agent in self.cfg.possible_agents], dim=-1) + # FIXME: This implementation assumes the spaces are fundamental ones. Fix it to support composite spaces + if isinstance(self.cfg.state_space, int) and self.cfg.state_space < 0: + self.state_buf = torch.cat( + [self.obs_dict[agent].reshape(self.num_envs, -1) for agent in self.cfg.possible_agents], dim=-1 + ) # compute and return custom environment state else: self.state_buf = self._get_states() @@ -568,25 +571,45 @@ def _configure_env_spaces(self): self.agents = self.cfg.possible_agents self.possible_agents = self.cfg.possible_agents + # show deprecation message and overwrite configuration + if self.cfg.num_actions is not None: + omni.log.warn("DirectMARLEnvCfg.num_actions is deprecated. Use DirectMARLEnvCfg.action_spaces instead.") + if isinstance(self.cfg.action_spaces, type(MISSING)): + self.cfg.action_spaces = self.cfg.num_actions + if self.cfg.num_observations is not None: + omni.log.warn( + "DirectMARLEnvCfg.num_observations is deprecated. Use DirectMARLEnvCfg.observation_spaces instead." + ) + if isinstance(self.cfg.observation_spaces, type(MISSING)): + self.cfg.observation_spaces = self.cfg.num_observations + if self.cfg.num_states is not None: + omni.log.warn("DirectMARLEnvCfg.num_states is deprecated. Use DirectMARLEnvCfg.state_space instead.") + if isinstance(self.cfg.state_space, type(MISSING)): + self.cfg.state_space = self.cfg.num_states + # set up observation and action spaces self.observation_spaces = { - agent: gym.spaces.Box(low=-np.inf, high=np.inf, shape=(self.cfg.num_observations[agent],)) - for agent in self.cfg.possible_agents + agent: spec_to_gym_space(self.cfg.observation_spaces[agent]) for agent in self.cfg.possible_agents } self.action_spaces = { - agent: gym.spaces.Box(low=-np.inf, high=np.inf, shape=(self.cfg.num_actions[agent],)) - for agent in self.cfg.possible_agents + agent: spec_to_gym_space(self.cfg.action_spaces[agent]) for agent in self.cfg.possible_agents } # set up state space - if not self.cfg.num_states: + if not self.cfg.state_space: self.state_space = None - if self.cfg.num_states < 0: - self.state_space = gym.spaces.Box( - low=-np.inf, high=np.inf, shape=(sum(self.cfg.num_observations.values()),) + if isinstance(self.cfg.state_space, int) and self.cfg.state_space < 0: + self.state_space = gym.spaces.flatten_space( + gym.spaces.Tuple([self.observation_spaces[agent] for agent in self.cfg.possible_agents]) ) else: - self.state_space = gym.spaces.Box(low=-np.inf, high=np.inf, shape=(self.cfg.num_states,)) + self.state_space = spec_to_gym_space(self.cfg.state_space) + + # instantiate actions (needed for tasks for which the observations computation is dependent on the actions) + self.actions = { + agent: sample_space(self.action_spaces[agent], self.sim.device, batch_size=self.num_envs, fill_value=0) + for agent in self.cfg.possible_agents + } def _reset_idx(self, env_ids: Sequence[int]): """Reset environments based on specified indices. @@ -664,8 +687,8 @@ def _get_observations(self) -> dict[AgentID, ObsType]: def _get_states(self) -> StateType: """Compute and return the states for the environment. - This method is only called (and therefore has to be implemented) when the :attr:`DirectMARLEnvCfg.num_states` - parameter is greater than zero. + This method is only called (and therefore has to be implemented) when the :attr:`DirectMARLEnvCfg.state_space` + parameter is not a number less than or equal to zero. Returns: The states for the environment. diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_marl_env_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_marl_env_cfg.py index 3dcf364f5c..40ecb64297 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_marl_env_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/direct_marl_env_cfg.py @@ -10,7 +10,7 @@ from omni.isaac.lab.utils import configclass from omni.isaac.lab.utils.noise import NoiseModelCfg -from .common import AgentID, ViewerCfg +from .common import AgentID, SpaceType, ViewerCfg from .ui import BaseEnvWindow @@ -104,11 +104,39 @@ class DirectMARLEnvCfg: Please refer to the :class:`omni.isaac.lab.managers.EventManager` class for more details. """ - num_observations: dict[AgentID, int] = MISSING - """The dimension of the observation space from each agent.""" + observation_spaces: dict[AgentID, SpaceType] = MISSING + """Observation space definition for each agent. + + The space can be defined either using Gymnasium :py:mod:`~gymnasium.spaces` (when a more detailed + specification of the space is desired) or basic Python data types (for simplicity). + + .. list-table:: + :header-rows: 1 + + * - Gymnasium space + - Python data type + * - :class:`~gymnasium.spaces.Box` + - Integer or list of integers (e.g.: ``7``, ``[64, 64, 3]``) + * - :class:`~gymnasium.spaces.Discrete` + - Single-element set (e.g.: ``{2}``) + * - :class:`~gymnasium.spaces.MultiDiscrete` + - List of single-element sets (e.g.: ``[{2}, {5}]``) + * - :class:`~gymnasium.spaces.Dict` + - Dictionary (e.g.: ``{"joints": 7, "rgb": [64, 64, 3], "gripper": {2}}``) + * - :class:`~gymnasium.spaces.Tuple` + - Tuple (e.g.: ``(7, [64, 64, 3], {2})``) + """ - num_states: int = MISSING - """The dimension of the state space from each environment instance. + num_observations: dict[AgentID, int] | None = None + """The dimension of the observation space for each agent. + + .. warning:: + + This attribute is deprecated. Use :attr:`~omni.isaac.lab.envs.DirectMARLEnvCfg.observation_spaces` instead. + """ + + state_space: SpaceType = MISSING + """State space definition. The following values are supported: @@ -116,6 +144,33 @@ class DirectMARLEnvCfg: * 0: No state-space will be constructed (`state_space` is None). This is useful to save computational resources when the algorithm to be trained does not need it. * greater than 0: Custom state-space dimension to be provided by the task implementation. + + The space can be defined either using Gymnasium :py:mod:`~gymnasium.spaces` (when a more detailed + specification of the space is desired) or basic Python data types (for simplicity). + + .. list-table:: + :header-rows: 1 + + * - Gymnasium space + - Python data type + * - :class:`~gymnasium.spaces.Box` + - Integer or list of integers (e.g.: ``7``, ``[64, 64, 3]``) + * - :class:`~gymnasium.spaces.Discrete` + - Single-element set (e.g.: ``{2}``) + * - :class:`~gymnasium.spaces.MultiDiscrete` + - List of single-element sets (e.g.: ``[{2}, {5}]``) + * - :class:`~gymnasium.spaces.Dict` + - Dictionary (e.g.: ``{"joints": 7, "rgb": [64, 64, 3], "gripper": {2}}``) + * - :class:`~gymnasium.spaces.Tuple` + - Tuple (e.g.: ``(7, [64, 64, 3], {2})``) + """ + + num_states: int | None = None + """The dimension of the state space from each environment instance. + + .. warning:: + + This attribute is deprecated. Use :attr:`~omni.isaac.lab.envs.DirectMARLEnvCfg.state_space` instead. """ observation_noise_model: dict[AgentID, NoiseModelCfg | None] | None = None @@ -124,8 +179,36 @@ class DirectMARLEnvCfg: Please refer to the :class:`omni.isaac.lab.utils.noise.NoiseModel` class for more details. """ - num_actions: dict[AgentID, int] = MISSING - """The dimension of the action space for each agent.""" + action_spaces: dict[AgentID, SpaceType] = MISSING + """Action space definition for each agent. + + The space can be defined either using Gymnasium :py:mod:`~gymnasium.spaces` (when a more detailed + specification of the space is desired) or basic Python data types (for simplicity). + + .. list-table:: + :header-rows: 1 + + * - Gymnasium space + - Python data type + * - :class:`~gymnasium.spaces.Box` + - Integer or list of integers (e.g.: ``7``, ``[64, 64, 3]``) + * - :class:`~gymnasium.spaces.Discrete` + - Single-element set (e.g.: ``{2}``) + * - :class:`~gymnasium.spaces.MultiDiscrete` + - List of single-element sets (e.g.: ``[{2}, {5}]``) + * - :class:`~gymnasium.spaces.Dict` + - Dictionary (e.g.: ``{"joints": 7, "rgb": [64, 64, 3], "gripper": {2}}``) + * - :class:`~gymnasium.spaces.Tuple` + - Tuple (e.g.: ``(7, [64, 64, 3], {2})``) + """ + + num_actions: dict[AgentID, int] | None = None + """The dimension of the action space for each agent. + + .. warning:: + + This attribute is deprecated. Use :attr:`~omni.isaac.lab.envs.DirectMARLEnvCfg.action_spaces` instead. + """ action_noise_model: dict[AgentID, NoiseModelCfg | None] | None = None """The noise model applied to the actions provided to the environment. Default is None, which means no noise is added. 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 656bf3e2dc..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 @@ -14,11 +14,12 @@ import weakref from abc import abstractmethod from collections.abc import Sequence +from dataclasses import MISSING from typing import Any, ClassVar -import carb import omni.isaac.core.utils.torch as torch_utils import omni.kit.app +import omni.log from omni.isaac.version import get_version from omni.isaac.lab.managers import EventManager @@ -30,6 +31,7 @@ from .common import VecEnvObs, VecEnvStepReturn from .direct_rl_env_cfg import DirectRLEnvCfg from .ui import ViewportCameraController +from .utils.spaces import sample_space, spec_to_gym_space class DirectRLEnv(gym.Env): @@ -77,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 @@ -88,7 +92,7 @@ def __init__(self, cfg: DirectRLEnvCfg, render_mode: str | None = None, **kwargs if self.cfg.seed is not None: self.cfg.seed = self.seed(self.cfg.seed) else: - carb.log_warn("Seed not set for the environment. The environment creation may not be deterministic.") + omni.log.warn("Seed not set for the environment. The environment creation may not be deterministic.") # create a simulation context to control the simulator if SimulationContext.instance() is None: @@ -107,10 +111,10 @@ def __init__(self, cfg: DirectRLEnvCfg, render_mode: str | None = None, **kwargs if self.cfg.sim.render_interval < self.cfg.decimation: msg = ( f"The render interval ({self.cfg.sim.render_interval}) is smaller than the decimation " - f"({self.cfg.decimation}). Multiple multiple render calls will happen for each environment step." + f"({self.cfg.decimation}). Multiple render calls will happen for each environment step." "If this is not intended, set the render interval to be equal to the decimation." ) - carb.log_warn(msg) + omni.log.warn(msg) # generate scene with Timer("[INFO]: Time taken for scene creation", "scene_creation"): @@ -171,7 +175,6 @@ def __init__(self, cfg: DirectRLEnvCfg, render_mode: str | None = None, **kwargs self.reset_terminated = torch.zeros(self.num_envs, device=self.device, dtype=torch.bool) self.reset_time_outs = torch.zeros_like(self.reset_terminated) self.reset_buf = torch.zeros(self.num_envs, dtype=torch.bool, device=self.sim.device) - self.actions = torch.zeros(self.num_envs, self.cfg.num_actions, device=self.sim.device) # setup the action and observation spaces for Gym self._configure_gym_env_spaces() @@ -507,27 +510,40 @@ def set_debug_vis(self, debug_vis: bool) -> bool: def _configure_gym_env_spaces(self): """Configure the action and observation spaces for the Gym environment.""" - # observation space (unbounded since we don't impose any limits) - self.num_actions = self.cfg.num_actions - self.num_observations = self.cfg.num_observations - self.num_states = self.cfg.num_states + # show deprecation message and overwrite configuration + if self.cfg.num_actions is not None: + omni.log.warn("DirectRLEnvCfg.num_actions is deprecated. Use DirectRLEnvCfg.action_space instead.") + if isinstance(self.cfg.action_space, type(MISSING)): + self.cfg.action_space = self.cfg.num_actions + if self.cfg.num_observations is not None: + omni.log.warn( + "DirectRLEnvCfg.num_observations is deprecated. Use DirectRLEnvCfg.observation_space instead." + ) + if isinstance(self.cfg.observation_space, type(MISSING)): + self.cfg.observation_space = self.cfg.num_observations + if self.cfg.num_states is not None: + omni.log.warn("DirectRLEnvCfg.num_states is deprecated. Use DirectRLEnvCfg.state_space instead.") + if isinstance(self.cfg.state_space, type(MISSING)): + self.cfg.state_space = self.cfg.num_states # set up spaces self.single_observation_space = gym.spaces.Dict() - self.single_observation_space["policy"] = gym.spaces.Box( - low=-np.inf, high=np.inf, shape=(self.num_observations,) - ) - self.single_action_space = gym.spaces.Box(low=-np.inf, high=np.inf, shape=(self.num_actions,)) + self.single_observation_space["policy"] = spec_to_gym_space(self.cfg.observation_space) + self.single_action_space = spec_to_gym_space(self.cfg.action_space) # batch the spaces for vectorized environments self.observation_space = gym.vector.utils.batch_space(self.single_observation_space["policy"], self.num_envs) self.action_space = gym.vector.utils.batch_space(self.single_action_space, self.num_envs) # optional state space for asymmetric actor-critic architectures - if self.num_states > 0: - self.single_observation_space["critic"] = gym.spaces.Box(low=-np.inf, high=np.inf, shape=(self.num_states,)) + self.state_space = None + if self.cfg.state_space: + self.single_observation_space["critic"] = spec_to_gym_space(self.cfg.state_space) self.state_space = gym.vector.utils.batch_space(self.single_observation_space["critic"], self.num_envs) + # instantiate actions (needed for tasks for which the observations computation is dependent on the actions) + self.actions = sample_space(self.single_action_space, self.sim.device, batch_size=self.num_envs, fill_value=0) + def _reset_idx(self, env_ids: Sequence[int]): """Reset environments based on specified indices. @@ -601,7 +617,7 @@ def _get_states(self) -> VecEnvObs | None: """Compute and return the states for the environment. The state-space is used for asymmetric actor-critic architectures. It is configured - using the :attr:`DirectRLEnvCfg.num_states` parameter. + using the :attr:`DirectRLEnvCfg.state_space` parameter. Returns: The states for the environment. If the environment does not have a state-space, the function 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 ad8c6c18c8..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 @@ -10,7 +10,7 @@ from omni.isaac.lab.utils import configclass from omni.isaac.lab.utils.noise import NoiseModelCfg -from .common import ViewerCfg +from .common import SpaceType, ViewerCfg from .ui import BaseEnvWindow @@ -98,19 +98,74 @@ 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. """ - num_observations: int = MISSING - """The dimension of the observation space from each environment instance.""" + observation_space: SpaceType = MISSING + """Observation space definition. + + The space can be defined either using Gymnasium :py:mod:`~gymnasium.spaces` (when a more detailed + specification of the space is desired) or basic Python data types (for simplicity). + + .. list-table:: + :header-rows: 1 + + * - Gymnasium space + - Python data type + * - :class:`~gymnasium.spaces.Box` + - Integer or list of integers (e.g.: ``7``, ``[64, 64, 3]``) + * - :class:`~gymnasium.spaces.Discrete` + - Single-element set (e.g.: ``{2}``) + * - :class:`~gymnasium.spaces.MultiDiscrete` + - List of single-element sets (e.g.: ``[{2}, {5}]``) + * - :class:`~gymnasium.spaces.Dict` + - Dictionary (e.g.: ``{"joints": 7, "rgb": [64, 64, 3], "gripper": {2}}``) + * - :class:`~gymnasium.spaces.Tuple` + - Tuple (e.g.: ``(7, [64, 64, 3], {2})``) + """ + + num_observations: int | None = None + """The dimension of the observation space from each environment instance. + + .. warning:: + + This attribute is deprecated. Use :attr:`~omni.isaac.lab.envs.DirectRLEnvCfg.observation_space` instead. + """ - num_states: int = 0 - """The dimension of the state-space from each environment instance. Default is 0, which means no state-space is defined. + state_space: SpaceType | None = None + """State space definition. This is useful for asymmetric actor-critic and defines the observation space for the critic. + + The space can be defined either using Gymnasium :py:mod:`~gymnasium.spaces` (when a more detailed + specification of the space is desired) or basic Python data types (for simplicity). + + .. list-table:: + :header-rows: 1 + + * - Gymnasium space + - Python data type + * - :class:`~gymnasium.spaces.Box` + - Integer or list of integers (e.g.: ``7``, ``[64, 64, 3]``) + * - :class:`~gymnasium.spaces.Discrete` + - Single-element set (e.g.: ``{2}``) + * - :class:`~gymnasium.spaces.MultiDiscrete` + - List of single-element sets (e.g.: ``[{2}, {5}]``) + * - :class:`~gymnasium.spaces.Dict` + - Dictionary (e.g.: ``{"joints": 7, "rgb": [64, 64, 3], "gripper": {2}}``) + * - :class:`~gymnasium.spaces.Tuple` + - Tuple (e.g.: ``(7, [64, 64, 3], {2})``) + """ + + num_states: int | None = None + """The dimension of the state-space from each environment instance. + + .. warning:: + + This attribute is deprecated. Use :attr:`~omni.isaac.lab.envs.DirectRLEnvCfg.state_space` instead. """ observation_noise_model: NoiseModelCfg | None = None @@ -119,8 +174,36 @@ class DirectRLEnvCfg: Please refer to the :class:`omni.isaac.lab.utils.noise.NoiseModel` class for more details. """ - num_actions: int = MISSING - """The dimension of the action space for each environment.""" + action_space: SpaceType = MISSING + """Action space definition. + + The space can be defined either using Gymnasium :py:mod:`~gymnasium.spaces` (when a more detailed + specification of the space is desired) or basic Python data types (for simplicity). + + .. list-table:: + :header-rows: 1 + + * - Gymnasium space + - Python data type + * - :class:`~gymnasium.spaces.Box` + - Integer or list of integers (e.g.: ``7``, ``[64, 64, 3]``) + * - :class:`~gymnasium.spaces.Discrete` + - Single-element set (e.g.: ``{2}``) + * - :class:`~gymnasium.spaces.MultiDiscrete` + - List of single-element sets (e.g.: ``[{2}, {5}]``) + * - :class:`~gymnasium.spaces.Dict` + - Dictionary (e.g.: ``{"joints": 7, "rgb": [64, 64, 3], "gripper": {2}}``) + * - :class:`~gymnasium.spaces.Tuple` + - Tuple (e.g.: ``(7, [64, 64, 3], {2})``) + """ + + num_actions: int | None = None + """The dimension of the action space for each environment. + + .. warning:: + + This attribute is deprecated. Use :attr:`~omni.isaac.lab.envs.DirectRLEnvCfg.action_space` instead. + """ action_noise_model: NoiseModelCfg | None = None """The noise model applied to the actions provided to the environment. Default is None, which means no noise is added. 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 4cc1b86b8c..548a3d2a6d 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 @@ -8,10 +8,10 @@ from collections.abc import Sequence from typing import Any -import carb import omni.isaac.core.utils.torch as torch_utils +import omni.log -from omni.isaac.lab.managers import ActionManager, EventManager, ObservationManager +from omni.isaac.lab.managers import ActionManager, EventManager, ObservationManager, RecorderManager from omni.isaac.lab.scene import InteractiveScene from omni.isaac.lab.sim import SimulationContext from omni.isaac.lab.utils.timer import Timer @@ -45,6 +45,9 @@ class ManagerBasedEnv: This includes resetting the scene to a default state, applying random pushes to the robot at different intervals of time, or randomizing properties such as mass and friction coefficients. This is useful for training and evaluating the robot in a variety of scenarios. + * **Recorder Manager**: The recorder manager that handles recording data produced during different steps + in the simulation. This includes recording in the beginning and end of a reset and a step. The recorded data + is distinguished per episode, per environment and can be exported through a dataset file handler to a file. The environment provides a unified interface for interacting with the simulation. However, it does not include task-specific quantities such as the reward function, or the termination conditions. These @@ -69,6 +72,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 @@ -78,7 +83,7 @@ def __init__(self, cfg: ManagerBasedEnvCfg): if self.cfg.seed is not None: self.cfg.seed = self.seed(self.cfg.seed) else: - carb.log_warn("Seed not set for the environment. The environment creation may not be deterministic.") + omni.log.warn("Seed not set for the environment. The environment creation may not be deterministic.") # create a simulation context to control the simulator if SimulationContext.instance() is None: @@ -103,10 +108,10 @@ def __init__(self, cfg: ManagerBasedEnvCfg): if self.cfg.sim.render_interval < self.cfg.decimation: msg = ( f"The render interval ({self.cfg.sim.render_interval}) is smaller than the decimation " - f"({self.cfg.decimation}). Multiple multiple render calls will happen for each environment step. " + f"({self.cfg.decimation}). Multiple render calls will happen for each environment step. " "If this is not intended, set the render interval to be equal to the decimation." ) - carb.log_warn(msg) + omni.log.warn(msg) # counter for simulation steps self._sim_step_counter = 0 @@ -151,6 +156,9 @@ def __init__(self, cfg: ManagerBasedEnvCfg): # allocate dictionary to store metrics self.extras = {} + # initialize observation buffers + self.obs_buf = {} + def __del__(self): """Cleanup for the environment.""" self.close() @@ -206,6 +214,9 @@ def load_managers(self): """ # prepare the managers + # -- recorder manager + self.recorder_manager = RecorderManager(self.cfg.recorders, self) + print("[INFO] Recorder Manager: ", self.recorder_manager) # -- action manager self.action_manager = ActionManager(self.cfg.actions, self) print("[INFO] Action Manager: ", self.action_manager) @@ -226,15 +237,18 @@ def load_managers(self): Operations - MDP. """ - def reset(self, seed: int | None = None, options: dict[str, Any] | None = None) -> tuple[VecEnvObs, dict]: - """Resets all the environments and returns observations. + def reset( + self, seed: int | None = None, env_ids: Sequence[int] | None = None, options: dict[str, Any] | None = None + ) -> tuple[VecEnvObs, dict]: + """Resets the specified environments and returns observations. - This function calls the :meth:`_reset_idx` function to reset all the environments. + This function calls the :meth:`_reset_idx` function to reset the specified environments. However, certain operations, such as procedural terrain generation, that happened during initialization are not repeated. Args: seed: The seed to use for randomization. Defaults to None, in which case the seed is not set. + env_ids: The environment ids to reset. Defaults to None, in which case all environments are reset. options: Additional information to specify how the environment is reset. Defaults to None. Note: @@ -243,20 +257,78 @@ def reset(self, seed: int | None = None, options: dict[str, Any] | None = None) Returns: A tuple containing the observations and extras. """ + if env_ids is None: + env_ids = torch.arange(self.num_envs, dtype=torch.int64, device=self.device) + + # trigger recorder terms for pre-reset calls + self.recorder_manager.record_pre_reset(env_ids) + # set the seed if seed is not None: self.seed(seed) # reset state of scene - indices = torch.arange(self.num_envs, dtype=torch.int64, device=self.device) - self._reset_idx(indices) + self._reset_idx(env_ids) + self.scene.write_data_to_sim() + + # trigger recorder terms for post-reset calls + self.recorder_manager.record_post_reset(env_ids) # if sensors are added to the scene, make sure we render to reflect changes in reset if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: self.sim.render() + # compute observations + self.obs_buf = self.observation_manager.compute() + # return observations - return self.observation_manager.compute(), self.extras + return self.obs_buf, self.extras + + def reset_to( + self, + state: dict[str, dict[str, dict[str, torch.Tensor]]], + env_ids: Sequence[int] | None, + seed: int | None = None, + is_relative: bool = False, + ) -> None: + """Resets specified environments to known states. + + Note that this is different from reset() function as it resets the environments to specific states + + Args: + state: The state to reset the specified environments to. + env_ids: The environment ids to reset. Defaults to None, in which case all environments are reset. + seed: The seed to use for randomization. Defaults to None, in which case the seed is not set. + is_relative: If set to True, the state is considered relative to the environment origins. Defaults to False. + """ + # reset all envs in the scene if env_ids is None + if env_ids is None: + env_ids = torch.arange(self.num_envs, dtype=torch.int64, device=self.device) + + # trigger recorder terms for pre-reset calls + self.recorder_manager.record_pre_reset(env_ids) + + # set the seed + if seed is not None: + self.seed(seed) + + self._reset_idx(env_ids) + + # set the state + self.scene.reset_to(state, env_ids, is_relative=is_relative) + + # trigger recorder terms for post-reset calls + self.recorder_manager.record_post_reset(env_ids) + + # if sensors are added to the scene, make sure we render to reflect changes in reset + if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: + self.sim.render() + + # compute observations + self.obs_buf = self.observation_manager.compute() + + # return observations + return self.obs_buf, self.extras def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]: """Execute one time-step of the environment's dynamics. @@ -276,6 +348,8 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]: # process actions self.action_manager.process_action(action.to(self.device)) + self.recorder_manager.record_pre_step() + # check if we need to do rendering within the physics loop # note: checked here once to avoid multiple checks within the loop is_rendering = self.sim.has_gui() or self.sim.has_rtx_sensors() @@ -301,8 +375,12 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]: if "interval" in self.event_manager.available_modes: self.event_manager.apply(mode="interval", dt=self.step_dt) + # -- compute observations + self.obs_buf = self.observation_manager.compute() + self.recorder_manager.record_post_step() + # return observations and extras - return self.observation_manager.compute(), self.extras + return self.obs_buf, self.extras @staticmethod def seed(seed: int = -1) -> int: @@ -332,6 +410,7 @@ def close(self): del self.action_manager del self.observation_manager del self.event_manager + del self.recorder_manager del self.scene # clear callbacks and instance self.sim.clear_all_callbacks() @@ -373,3 +452,6 @@ def _reset_idx(self, env_ids: Sequence[int]): # -- event manager info = self.event_manager.reset(env_ids) self.extras["log"].update(info) + # -- recorder manager + info = self.recorder_manager.reset(env_ids) + self.extras["log"].update(info) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_env_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_env_cfg.py index 4c6436c9dd..6c64927dd7 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_env_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_env_cfg.py @@ -13,6 +13,7 @@ import omni.isaac.lab.envs.mdp as mdp from omni.isaac.lab.managers import EventTermCfg as EventTerm +from omni.isaac.lab.managers import RecorderManagerBaseCfg as DefaultEmptyRecorderManagerCfg from omni.isaac.lab.scene import InteractiveSceneCfg from omni.isaac.lab.sim import SimulationCfg from omni.isaac.lab.utils import configclass @@ -78,6 +79,12 @@ class ManagerBasedEnvCfg: Please refer to the :class:`omni.isaac.lab.scene.InteractiveSceneCfg` class for more details. """ + recorders: object = DefaultEmptyRecorderManagerCfg() + """Recorder settings. Defaults to recording nothing. + + Please refer to the :class:`omni.isaac.lab.managers.RecorderManager` class for more details. + """ + observations: object = MISSING """Observation space settings. diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_rl_env.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_rl_env.py index af059866ac..d21700168f 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_rl_env.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/manager_based_rl_env.py @@ -158,6 +158,8 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: # process actions self.action_manager.process_action(action.to(self.device)) + self.recorder_manager.record_pre_step() + # check if we need to do rendering within the physics loop # note: checked here once to avoid multiple checks within the loop is_rendering = self.sim.has_gui() or self.sim.has_rtx_sensors() @@ -190,14 +192,29 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: # -- reward computation self.reward_buf = self.reward_manager.compute(dt=self.step_dt) + if len(self.recorder_manager.active_terms) > 0: + # update observations for recording if needed + self.obs_buf = self.observation_manager.compute() + self.recorder_manager.record_post_step() + # -- reset envs that terminated/timed-out and log the episode information reset_env_ids = self.reset_buf.nonzero(as_tuple=False).squeeze(-1) if len(reset_env_ids) > 0: + # trigger recorder terms for pre-reset calls + self.recorder_manager.record_pre_reset(reset_env_ids) + self._reset_idx(reset_env_ids) + + # this is needed to make joint positions set from reset events effective + self.scene.write_data_to_sim() + # if sensors are added to the scene, make sure we render to reflect changes in reset if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: self.sim.render() + # trigger recorder terms for post-reset calls + self.recorder_manager.record_post_reset(reset_env_ids) + # -- update command self.command_manager.compute(dt=self.step_dt) # -- step interval events @@ -355,6 +372,9 @@ def _reset_idx(self, env_ids: Sequence[int]): # -- termination manager info = self.termination_manager.reset(env_ids) self.extras["log"].update(info) + # -- recorder manager + info = self.recorder_manager.reset(env_ids) + self.extras["log"].update(info) # reset the episode length buffer self.episode_length_buf[env_ids] = 0 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/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/__init__.py index f46bf684b6..d02079a0df 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/__init__.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/__init__.py @@ -20,5 +20,6 @@ from .curriculums import * # noqa: F401, F403 from .events import * # noqa: F401, F403 from .observations import * # noqa: F401, F403 +from .recorders import * # noqa: F401, F403 from .rewards import * # noqa: F401, F403 from .terminations import * # noqa: F401, F403 diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/binary_joint_actions.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/binary_joint_actions.py index 73b2353add..5c2ba3fa15 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/binary_joint_actions.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/binary_joint_actions.py @@ -9,7 +9,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -import carb +import omni.log import omni.isaac.lab.utils.string as string_utils from omni.isaac.lab.assets.articulation import Articulation @@ -52,7 +52,7 @@ def __init__(self, cfg: actions_cfg.BinaryJointActionCfg, env: ManagerBasedEnv) self._joint_ids, self._joint_names = self._asset.find_joints(self.cfg.joint_names) self._num_joints = len(self._joint_ids) # log the resolved joint names for debugging - carb.log_info( + omni.log.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/joint_actions.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/joint_actions.py index d0ebd20d2b..ee5586b7f2 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/joint_actions.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/joint_actions.py @@ -9,7 +9,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -import carb +import omni.log import omni.isaac.lab.utils.string as string_utils from omni.isaac.lab.assets.articulation import Articulation @@ -61,7 +61,7 @@ def __init__(self, cfg: actions_cfg.JointActionCfg, env: ManagerBasedEnv) -> Non ) self._num_joints = len(self._joint_ids) # log the resolved joint names for debugging - carb.log_info( + omni.log.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) 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 81f4a82184..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 @@ -9,7 +9,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -import carb +import omni.log import omni.isaac.lab.utils.math as math_utils import omni.isaac.lab.utils.string as string_utils @@ -53,7 +53,7 @@ def __init__(self, cfg: actions_cfg.JointPositionToLimitsActionCfg, env: Manager self._joint_ids, self._joint_names = self._asset.find_joints(self.cfg.joint_names) self._num_joints = len(self._joint_ids) # log the resolved joint names for debugging - carb.log_info( + omni.log.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) @@ -183,6 +183,8 @@ 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, self._joint_ids] diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/non_holonomic_actions.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/non_holonomic_actions.py index 6344ad256a..fc9ed89d6e 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/non_holonomic_actions.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/non_holonomic_actions.py @@ -9,7 +9,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -import carb +import omni.log from omni.isaac.lab.assets.articulation import Articulation from omni.isaac.lab.managers.action_manager import ActionTerm @@ -88,11 +88,11 @@ def __init__(self, cfg: actions_cfg.NonHolonomicActionCfg, env: ManagerBasedEnv) self._joint_ids = [x_joint_id[0], y_joint_id[0], yaw_joint_id[0]] self._joint_names = [x_joint_name[0], y_joint_name[0], yaw_joint_name[0]] # log info for debugging - carb.log_info( + omni.log.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) - carb.log_info( + omni.log.info( f"Resolved body name for the action term {self.__class__.__name__}: {self._body_name} [{self._body_idx}]" ) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/task_space_actions.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/task_space_actions.py index e350514b91..a8a1108f50 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/task_space_actions.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/actions/task_space_actions.py @@ -9,7 +9,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -import carb +import omni.log import omni.isaac.lab.utils.math as math_utils from omni.isaac.lab.assets.articulation import Articulation @@ -70,11 +70,11 @@ def __init__(self, cfg: actions_cfg.DifferentialInverseKinematicsActionCfg, env: self._jacobi_joint_ids = [i + 6 for i in self._joint_ids] # log info for debugging - carb.log_info( + omni.log.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) - carb.log_info( + omni.log.info( f"Resolved body name for the action term {self.__class__.__name__}: {self._body_name} [{self._body_idx}]" ) # Avoid indexing across all joints for efficiency @@ -117,6 +117,19 @@ def raw_actions(self) -> torch.Tensor: def processed_actions(self) -> torch.Tensor: return self._processed_actions + @property + def jacobian_w(self) -> torch.Tensor: + return self._asset.root_physx_view.get_jacobians()[:, self._jacobi_body_idx, :, self._jacobi_joint_ids] + + @property + def jacobian_b(self) -> torch.Tensor: + jacobian = self.jacobian_w + base_rot = self._asset.data.root_quat_w + base_rot_matrix = math_utils.matrix_from_quat(math_utils.quat_inv(base_rot)) + jacobian[:, :3, :] = torch.bmm(base_rot_matrix, jacobian[:, :3, :]) + jacobian[:, 3:, :] = torch.bmm(base_rot_matrix, jacobian[:, 3:, :]) + return jacobian + """ Operations. """ @@ -178,7 +191,7 @@ def _compute_frame_jacobian(self): the right Jacobian from the parent body Jacobian. """ # read the parent jacobian - jacobian = self._asset.root_physx_view.get_jacobians()[:, self._jacobi_body_idx, :, self._jacobi_joint_ids] + jacobian = self.jacobian_b # account for the offset if self.cfg.body_offset is not None: # Modify the jacobian to account for the offset 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/envs/mdp/events.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/events.py index cb55b7a779..41000003dc 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 @@ -25,22 +24,14 @@ 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: from omni.isaac.lab.envs import ManagerBasedEnv -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 @@ -53,6 +44,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 @@ -60,69 +55,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( @@ -263,54 +300,57 @@ def randomize_actuator_gains( .. tip:: For implicit actuators, this function uses CPU tensors to assign the actuator gains into the simulation. In such cases, it is recommended to use this function only during the initialization of the environment. - - Raises: - NotImplementedError: If the joint indices are in explicit motor mode. This operation is currently - not supported for explicit actuator models. """ - # extract the used quantities (to enable type-hinting) + # Extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] - # resolve environment ids + # Resolve environment ids if env_ids is None: env_ids = torch.arange(env.scene.num_envs, device=asset.device) - # resolve joint indices - if asset_cfg.joint_ids == slice(None): - joint_ids_list = range(asset.num_joints) - joint_ids = slice(None) # for optimization purposes - else: - joint_ids_list = asset_cfg.joint_ids - joint_ids = torch.tensor(asset_cfg.joint_ids, dtype=torch.int, device=asset.device) - - # check if none of the joint indices are in explicit motor mode - for joint_index in joint_ids_list: - for act_name, actuator in asset.actuators.items(): - # if joint indices are a slice (i.e., all joints are captured) or the joint index is in the actuator - if actuator.joint_indices == slice(None) or joint_index in actuator.joint_indices: - if not isinstance(actuator, ImplicitActuator): - raise NotImplementedError( - "Event term 'randomize_actuator_stiffness_and_damping' is performed on asset" - f" '{asset_cfg.name}' on the joint '{asset.joint_names[joint_index]}' ('{joint_index}') which" - f" uses an explicit actuator model '{act_name}<{actuator.__class__.__name__}>'. This operation" - " is currently not supported for explicit actuator models." - ) + def randomize(data: torch.Tensor, params: tuple[float, float]) -> torch.Tensor: + return _randomize_prop_by_op( + data, params, dim_0_ids=None, dim_1_ids=actuator_indices, operation=operation, distribution=distribution + ) - # sample joint properties from the given ranges and set into the physics simulation - # -- stiffness - if stiffness_distribution_params is not None: - stiffness = asset.data.default_joint_stiffness.to(asset.device).clone() - stiffness = _randomize_prop_by_op( - stiffness, stiffness_distribution_params, env_ids, joint_ids, operation=operation, distribution=distribution - )[env_ids][:, joint_ids] - asset.write_joint_stiffness_to_sim(stiffness, joint_ids=joint_ids, env_ids=env_ids) - # -- damping - if damping_distribution_params is not None: - damping = asset.data.default_joint_damping.to(asset.device).clone() - damping = _randomize_prop_by_op( - damping, damping_distribution_params, env_ids, joint_ids, operation=operation, distribution=distribution - )[env_ids][:, joint_ids] - asset.write_joint_damping_to_sim(damping, joint_ids=joint_ids, env_ids=env_ids) + # Loop through actuators and randomize gains + for actuator in asset.actuators.values(): + if isinstance(asset_cfg.joint_ids, slice): + # we take all the joints of the actuator + actuator_indices = slice(None) + if isinstance(actuator.joint_indices, slice): + global_indices = slice(None) + else: + global_indices = torch.tensor(actuator.joint_indices, device=asset.device) + elif isinstance(actuator.joint_indices, slice): + # we take the joints defined in the asset config + global_indices = actuator_indices = torch.tensor(asset_cfg.joint_ids, device=asset.device) + else: + # we take the intersection of the actuator joints and the asset config joints + actuator_joint_indices = torch.tensor(actuator.joint_indices, device=asset.device) + asset_joint_ids = torch.tensor(asset_cfg.joint_ids, device=asset.device) + # the indices of the joints in the actuator that have to be randomized + actuator_indices = torch.nonzero(torch.isin(actuator_joint_indices, asset_joint_ids)).view(-1) + if len(actuator_indices) == 0: + continue + # maps actuator indices that have to be randomized to global joint indices + global_indices = actuator_joint_indices[actuator_indices] + # Randomize stiffness + if stiffness_distribution_params is not None: + stiffness = actuator.stiffness[env_ids].clone() + stiffness[:, actuator_indices] = asset.data.default_joint_stiffness[env_ids][:, global_indices].clone() + randomize(stiffness, stiffness_distribution_params) + actuator.stiffness[env_ids] = stiffness + if isinstance(actuator, ImplicitActuator): + asset.write_joint_stiffness_to_sim(stiffness, joint_ids=actuator.joint_indices, env_ids=env_ids) + # Randomize damping + if damping_distribution_params is not None: + damping = actuator.damping[env_ids].clone() + damping[:, actuator_indices] = asset.data.default_joint_damping[env_ids][:, global_indices].clone() + randomize(damping, damping_distribution_params) + actuator.damping[env_ids] = damping + if isinstance(actuator, ImplicitActuator): + asset.write_joint_damping_to_sim(damping, joint_ids=actuator.joint_indices, env_ids=env_ids) def randomize_joint_parameters( @@ -964,7 +1004,8 @@ def _randomize_prop_by_op( dim_0_ids = slice(None) else: n_dim_0 = len(dim_0_ids) - dim_0_ids = dim_0_ids[:, None] + if not isinstance(dim_1_ids, slice): + dim_0_ids = dim_0_ids[:, None] # -- dim 1 if isinstance(dim_1_ids, slice): n_dim_1 = data.shape[1] diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/observations.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/observations.py index c770915337..2c97cf151e 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/observations.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/observations.py @@ -17,11 +17,14 @@ import omni.isaac.lab.utils.math as math_utils from omni.isaac.lab.assets import Articulation, RigidObject from omni.isaac.lab.managers import SceneEntityCfg -from omni.isaac.lab.sensors import Camera, RayCaster, RayCasterCamera, TiledCamera +from omni.isaac.lab.managers.manager_base import ManagerTermBase +from omni.isaac.lab.managers.manager_term_cfg import ObservationTermCfg +from omni.isaac.lab.sensors import Camera, Imu, RayCaster, RayCasterCamera, TiledCamera if TYPE_CHECKING: from omni.isaac.lab.envs import ManagerBasedEnv, ManagerBasedRLEnv + """ Root state. """ @@ -182,6 +185,52 @@ def body_incoming_wrench(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg) -> tor return link_incoming_forces.view(env.num_envs, -1) +def imu_orientation(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("imu")) -> torch.Tensor: + """Imu sensor orientation in the simulation world frame. + + Args: + env: The environment. + asset_cfg: The SceneEntity associated with an IMU sensor. Defaults to SceneEntityCfg("imu"). + + Returns: + Orientation in the world frame in (w, x, y, z) quaternion form. Shape is (num_envs, 4). + """ + # extract the used quantities (to enable type-hinting) + asset: Imu = env.scene[asset_cfg.name] + # return the orientation quaternion + return asset.data.quat_w + + +def imu_ang_vel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("imu")) -> torch.Tensor: + """Imu sensor angular velocity w.r.t. environment origin expressed in the sensor frame. + + Args: + env: The environment. + asset_cfg: The SceneEntity associated with an IMU sensor. Defaults to SceneEntityCfg("imu"). + + Returns: + The angular velocity (rad/s) in the sensor frame. Shape is (num_envs, 3). + """ + # extract the used quantities (to enable type-hinting) + asset: Imu = env.scene[asset_cfg.name] + # return the angular velocity + return asset.data.ang_vel_b + + +def imu_lin_acc(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("imu")) -> torch.Tensor: + """Imu sensor linear acceleration w.r.t. the environment origin expressed in sensor frame. + + Args: + env: The environment. + asset_cfg: The SceneEntity associated with an IMU sensor. Defaults to SceneEntityCfg("imu"). + + Returns: + The linear acceleration (m/s^2) in the sensor frame. Shape is (num_envs, 3). + """ + asset: Imu = env.scene[asset_cfg.name] + return asset.data.lin_acc_b + + def image( env: ManagerBasedEnv, sensor_cfg: SceneEntityCfg = SceneEntityCfg("tiled_camera"), @@ -231,6 +280,230 @@ def image( return images.clone() +class image_features(ManagerTermBase): + """Extracted image features from a pre-trained frozen encoder. + + This term uses models from the model zoo in PyTorch and extracts features from the images. + + It calls the :func:`image` function to get the images and then processes them using the model zoo. + + A user can provide their own model zoo configuration to use different models for feature extraction. + The model zoo configuration should be a dictionary that maps different model names to a dictionary + that defines the model, preprocess and inference functions. The dictionary should have the following + entries: + + - "model": A callable that returns the model when invoked without arguments. + - "reset": A callable that resets the model. This is useful when the model has a state that needs to be reset. + - "inference": A callable that, when given the model and the images, returns the extracted features. + + If the model zoo configuration is not provided, the default model zoo configurations are used. The default + model zoo configurations include the models from Theia :cite:`shang2024theia` and ResNet :cite:`he2016deep`. + These models are loaded from `Hugging-Face transformers `_ and + `PyTorch torchvision `_ respectively. + + Args: + sensor_cfg: The sensor configuration to poll. Defaults to SceneEntityCfg("tiled_camera"). + data_type: The sensor data type. Defaults to "rgb". + convert_perspective_to_orthogonal: Whether to orthogonalize perspective depth images. + This is used only when the data type is "distance_to_camera". Defaults to False. + model_zoo_cfg: A user-defined dictionary that maps different model names to their respective configurations. + Defaults to None. If None, the default model zoo configurations are used. + model_name: The name of the model to use for inference. Defaults to "resnet18". + model_device: The device to store and infer the model on. This is useful when offloading the computation + from the environment simulation device. Defaults to the environment device. + inference_kwargs: Additional keyword arguments to pass to the inference function. Defaults to None, + which means no additional arguments are passed. + + Returns: + The extracted features tensor. Shape is (num_envs, feature_dim). + + Raises: + ValueError: When the model name is not found in the provided model zoo configuration. + ValueError: When the model name is not found in the default model zoo configuration. + """ + + def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedEnv): + # initialize the base class + super().__init__(cfg, env) + + # extract parameters from the configuration + self.model_zoo_cfg: dict = cfg.params.get("model_zoo_cfg") # type: ignore + self.model_name: str = cfg.params.get("model_name", "resnet18") # type: ignore + self.model_device: str = cfg.params.get("model_device", env.device) # type: ignore + + # List of Theia models - These are configured through `_prepare_theia_transformer_model` function + default_theia_models = [ + "theia-tiny-patch16-224-cddsv", + "theia-tiny-patch16-224-cdiv", + "theia-small-patch16-224-cdiv", + "theia-base-patch16-224-cdiv", + "theia-small-patch16-224-cddsv", + "theia-base-patch16-224-cddsv", + ] + # List of ResNet models - These are configured through `_prepare_resnet_model` function + default_resnet_models = ["resnet18", "resnet34", "resnet50", "resnet101"] + + # Check if model name is specified in the model zoo configuration + if self.model_zoo_cfg is not None and self.model_name not in self.model_zoo_cfg: + raise ValueError( + f"Model name '{self.model_name}' not found in the provided model zoo configuration." + " Please add the model to the model zoo configuration or use a different model name." + f" Available models in the provided list: {list(self.model_zoo_cfg.keys())}." + "\nHint: If you want to use a default model, consider using one of the following models:" + f" {default_theia_models + default_resnet_models}. In this case, you can remove the" + " 'model_zoo_cfg' parameter from the observation term configuration." + ) + if self.model_zoo_cfg is None: + if self.model_name in default_theia_models: + model_config = self._prepare_theia_transformer_model(self.model_name, self.model_device) + elif self.model_name in default_resnet_models: + model_config = self._prepare_resnet_model(self.model_name, self.model_device) + else: + raise ValueError( + f"Model name '{self.model_name}' not found in the default model zoo configuration." + f" Available models: {default_theia_models + default_resnet_models}." + ) + else: + model_config = self.model_zoo_cfg[self.model_name] + + # Retrieve the model, preprocess and inference functions + self._model = model_config["model"]() + self._reset_fn = model_config.get("reset") + self._inference_fn = model_config["inference"] + + def reset(self, env_ids: torch.Tensor | None = None): + # reset the model if a reset function is provided + # this might be useful when the model has a state that needs to be reset + # for example: video transformers + if self._reset_fn is not None: + self._reset_fn(self._model, env_ids) + + def __call__( + self, + env: ManagerBasedEnv, + sensor_cfg: SceneEntityCfg = SceneEntityCfg("tiled_camera"), + data_type: str = "rgb", + convert_perspective_to_orthogonal: bool = False, + model_zoo_cfg: dict | None = None, + model_name: str = "resnet18", + model_device: str | None = None, + inference_kwargs: dict | None = None, + ) -> torch.Tensor: + # obtain the images from the sensor + image_data = image( + env=env, + sensor_cfg=sensor_cfg, + data_type=data_type, + convert_perspective_to_orthogonal=convert_perspective_to_orthogonal, + normalize=False, # we pre-process based on model + ) + # store the device of the image + image_device = image_data.device + # forward the images through the model + features = self._inference_fn(self._model, image_data, **(inference_kwargs or {})) + + # move the features back to the image device + return features.detach().to(image_device) + + """ + Helper functions. + """ + + def _prepare_theia_transformer_model(self, model_name: str, model_device: str) -> dict: + """Prepare the Theia transformer model for inference. + + Args: + model_name: The name of the Theia transformer model to prepare. + model_device: The device to store and infer the model on. + + Returns: + A dictionary containing the model and inference functions. + """ + from transformers import AutoModel + + def _load_model() -> torch.nn.Module: + """Load the Theia transformer model.""" + model = AutoModel.from_pretrained(f"theaiinstitute/{model_name}", trust_remote_code=True).eval() + return model.to(model_device) + + def _inference(model, images: torch.Tensor) -> torch.Tensor: + """Inference the Theia transformer model. + + Args: + model: The Theia transformer model. + images: The preprocessed image tensor. Shape is (num_envs, height, width, channel). + + Returns: + The extracted features tensor. Shape is (num_envs, feature_dim). + """ + # Move the image to the model device + image_proc = images.to(model_device) + # permute the image to (num_envs, channel, height, width) + image_proc = image_proc.permute(0, 3, 1, 2).float() / 255.0 + # Normalize the image + mean = torch.tensor([0.485, 0.456, 0.406], device=model_device).view(1, 3, 1, 1) + std = torch.tensor([0.229, 0.224, 0.225], device=model_device).view(1, 3, 1, 1) + image_proc = (image_proc - mean) / std + + # Taken from Transformers; inference converted to be GPU only + features = model.backbone.model(pixel_values=image_proc, interpolate_pos_encoding=True) + return features.last_hidden_state[:, 1:] + + # return the model, preprocess and inference functions + return {"model": _load_model, "inference": _inference} + + def _prepare_resnet_model(self, model_name: str, model_device: str) -> dict: + """Prepare the ResNet model for inference. + + Args: + model_name: The name of the ResNet model to prepare. + model_device: The device to store and infer the model on. + + Returns: + A dictionary containing the model and inference functions. + """ + from torchvision import models + + def _load_model() -> torch.nn.Module: + """Load the ResNet model.""" + # map the model name to the weights + resnet_weights = { + "resnet18": "ResNet18_Weights.IMAGENET1K_V1", + "resnet34": "ResNet34_Weights.IMAGENET1K_V1", + "resnet50": "ResNet50_Weights.IMAGENET1K_V1", + "resnet101": "ResNet101_Weights.IMAGENET1K_V1", + } + + # load the model + model = getattr(models, model_name)(weights=resnet_weights[model_name]).eval() + return model.to(model_device) + + def _inference(model, images: torch.Tensor) -> torch.Tensor: + """Inference the ResNet model. + + Args: + model: The ResNet model. + images: The preprocessed image tensor. Shape is (num_envs, channel, height, width). + + Returns: + The extracted features tensor. Shape is (num_envs, feature_dim). + """ + # move the image to the model device + image_proc = images.to(model_device) + # permute the image to (num_envs, channel, height, width) + image_proc = image_proc.permute(0, 3, 1, 2).float() / 255.0 + # normalize the image + mean = torch.tensor([0.485, 0.456, 0.406], device=model_device).view(1, 3, 1, 1) + std = torch.tensor([0.229, 0.224, 0.225], device=model_device).view(1, 3, 1, 1) + image_proc = (image_proc - mean) / std + + # forward the image through the model + return model(image_proc) + + # return the model, preprocess and inference functions + return {"model": _load_model, "inference": _inference} + + """ Actions. """ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/recorders/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/recorders/__init__.py new file mode 100644 index 0000000000..207a732367 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/recorders/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Various recorder terms that can be used in the environment.""" + +from .recorders import * +from .recorders_cfg import * diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/recorders/recorders.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/recorders/recorders.py new file mode 100644 index 0000000000..026c22f9a3 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/recorders/recorders.py @@ -0,0 +1,44 @@ +# Copyright (c) 2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from collections.abc import Sequence + +from omni.isaac.lab.managers.recorder_manager import RecorderTerm + + +class InitialStateRecorder(RecorderTerm): + """Recorder term that records the initial state of the environment after reset.""" + + def record_post_reset(self, env_ids: Sequence[int] | None): + def extract_env_ids_values(value): + nonlocal env_ids + if isinstance(value, dict): + return {k: extract_env_ids_values(v) for k, v in value.items()} + return value[env_ids] + + return "initial_state", extract_env_ids_values(self._env.scene.get_state(is_relative=True)) + + +class PostStepStatesRecorder(RecorderTerm): + """Recorder term that records the state of the environment at the end of each step.""" + + def record_post_step(self): + return "states", self._env.scene.get_state(is_relative=True) + + +class PreStepActionsRecorder(RecorderTerm): + """Recorder term that records the actions in the beginning of each step.""" + + def record_pre_step(self): + return "actions", self._env.action_manager.action + + +class PreStepFlatPolicyObservationsRecorder(RecorderTerm): + """Recorder term that records the policy group observations in each step.""" + + def record_pre_step(self): + return "obs", self._env.obs_buf["policy"] diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/recorders/recorders_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/recorders/recorders_cfg.py new file mode 100644 index 0000000000..16c1eee34a --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/recorders/recorders_cfg.py @@ -0,0 +1,56 @@ +# Copyright (c) 2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from omni.isaac.lab.managers.recorder_manager import RecorderManagerBaseCfg, RecorderTerm, RecorderTermCfg +from omni.isaac.lab.utils import configclass + +from . import recorders + +## +# State recorders. +## + + +@configclass +class InitialStateRecorderCfg(RecorderTermCfg): + """Configuration for the initial state recorder term.""" + + class_type: type[RecorderTerm] = recorders.InitialStateRecorder + + +@configclass +class PostStepStatesRecorderCfg(RecorderTermCfg): + """Configuration for the step state recorder term.""" + + class_type: type[RecorderTerm] = recorders.PostStepStatesRecorder + + +@configclass +class PreStepActionsRecorderCfg(RecorderTermCfg): + """Configuration for the step action recorder term.""" + + class_type: type[RecorderTerm] = recorders.PreStepActionsRecorder + + +@configclass +class PreStepFlatPolicyObservationsRecorderCfg(RecorderTermCfg): + """Configuration for the step policy observation recorder term.""" + + class_type: type[RecorderTerm] = recorders.PreStepFlatPolicyObservationsRecorder + + +## +# Recorder manager configurations. +## + + +@configclass +class ActionStateRecorderManagerCfg(RecorderManagerBaseCfg): + """Recorder configurations for recording actions and states.""" + + record_initial_state = InitialStateRecorderCfg() + record_post_step_states = PostStepStatesRecorderCfg() + record_pre_step_actions = PreStepActionsRecorderCfg() + record_pre_step_flat_policy_observations = PreStepFlatPolicyObservationsRecorderCfg() diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/rewards.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/rewards.py index 7f03bf41f0..1a68d321ab 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/rewards.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/rewards.py @@ -159,7 +159,7 @@ def joint_acc_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntity return torch.sum(torch.square(asset.data.joint_acc[:, asset_cfg.joint_ids]), dim=1) -def joint_deviation_l1(env, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: +def joint_deviation_l1(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Penalize joint positions that deviate from the default one.""" # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/ui/base_env_window.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/ui/base_env_window.py index a37a8723ec..850ad0a355 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/ui/base_env_window.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/ui/base_env_window.py @@ -382,7 +382,7 @@ def _create_debug_vis_ui_element(self, name: str, elem: object): self.ui_window_elements[f"{name}_cb"] = SimpleCheckBox( model=omni.ui.SimpleBoolModel(), enabled=elem.has_debug_vis_implementation, - checked=elem.cfg.debug_vis, + checked=elem.cfg.debug_vis if elem.cfg else False, on_checked_fn=lambda value, e=weakref.proxy(elem): e.set_debug_vis(value), ) omni.isaac.ui.ui_utils.add_line_rect_flourish() diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils/__init__.py new file mode 100644 index 0000000000..913e1edb90 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Sub-package for environment utils.""" diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils/marl.py similarity index 76% rename from source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils.py rename to source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils/marl.py index cacbdeaf81..46519048ae 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils/marl.py @@ -9,9 +9,9 @@ import torch from typing import Any -from .common import ActionType, AgentID, EnvStepReturn, ObsType, StateType, VecEnvObs, VecEnvStepReturn -from .direct_marl_env import DirectMARLEnv -from .direct_rl_env import DirectRLEnv +from ..common import ActionType, AgentID, EnvStepReturn, ObsType, StateType, VecEnvObs, VecEnvStepReturn +from ..direct_marl_env import DirectMARLEnv +from ..direct_rl_env import DirectRLEnv def multi_agent_to_single_agent(env: DirectMARLEnv, state_as_observation: bool = False) -> DirectRLEnv: @@ -39,7 +39,7 @@ def multi_agent_to_single_agent(env: DirectMARLEnv, state_as_observation: bool = Raises: AssertionError: If the environment state cannot be used as observation since it was explicitly defined - as unconstructed (:attr:`DirectMARLEnvCfg.num_states`). + as unconstructed (:attr:`DirectMARLEnvCfg.state_space`). """ class Env(DirectRLEnv): @@ -49,7 +49,7 @@ def __init__(self, env: DirectMARLEnv) -> None: # check if it is possible to use the multi-agent environment state as single-agent observation self._state_as_observation = state_as_observation if self._state_as_observation: - assert self.env.cfg.num_states != 0, ( + assert self.env.cfg.state_space != 0, ( "The environment state cannot be used as observation since it was explicitly defined as" " unconstructed" ) @@ -58,18 +58,17 @@ def __init__(self, env: DirectMARLEnv) -> None: self.cfg = self.env.cfg self.sim = self.env.sim self.scene = self.env.scene - self.num_actions = sum(self.env.cfg.num_actions.values()) - self.num_observations = sum(self.env.cfg.num_observations.values()) - self.num_states = self.env.cfg.num_states self.single_observation_space = gym.spaces.Dict() if self._state_as_observation: self.single_observation_space["policy"] = self.env.state_space else: - self.single_observation_space["policy"] = gym.spaces.Box( - low=-np.inf, high=np.inf, shape=(self.num_observations,) + self.single_observation_space["policy"] = gym.spaces.flatten_space( + gym.spaces.Tuple([self.env.observation_spaces[agent] for agent in self.env.possible_agents]) ) - self.single_action_space = gym.spaces.Box(low=-np.inf, high=np.inf, shape=(self.num_actions,)) + self.single_action_space = gym.spaces.flatten_space( + gym.spaces.Tuple([self.env.action_spaces[agent] for agent in self.env.possible_agents]) + ) # batch the spaces for vectorized environments self.observation_space = gym.vector.utils.batch_space( @@ -84,18 +83,25 @@ def reset(self, seed: int | None = None, options: dict[str, Any] | None = None) if self._state_as_observation: obs = {"policy": self.env.state()} # concatenate agents' observations + # FIXME: This implementation assumes the spaces are fundamental ones. Fix it to support composite spaces else: - obs = {"policy": torch.cat([obs[agent] for agent in self.env.possible_agents], dim=-1)} + obs = { + "policy": torch.cat( + [obs[agent].reshape(self.num_envs, -1) for agent in self.env.possible_agents], dim=-1 + ) + } return obs, extras def step(self, action: torch.Tensor) -> VecEnvStepReturn: # split single-agent actions to build the multi-agent ones + # FIXME: This implementation assumes the spaces are fundamental ones. Fix it to support composite spaces index = 0 _actions = {} for agent in self.env.possible_agents: - _actions[agent] = action[:, index : index + self.env.cfg.num_actions[agent]] - index += self.env.cfg.num_actions[agent] + delta = gym.spaces.flatdim(self.env.action_spaces[agent]) + _actions[agent] = action[:, index : index + delta] + index += delta # step the environment obs, rewards, terminated, time_outs, extras = self.env.step(_actions) @@ -104,8 +110,13 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: if self._state_as_observation: obs = {"policy": self.env.state()} # concatenate agents' observations + # FIXME: This implementation assumes the spaces are fundamental ones. Fix it to support composite spaces else: - obs = {"policy": torch.cat([obs[agent] for agent in self.env.possible_agents], dim=-1)} + obs = { + "policy": torch.cat( + [obs[agent].reshape(self.num_envs, -1) for agent in self.env.possible_agents], dim=-1 + ) + } # process environment outputs to return single-agent data rewards = sum(rewards.values()) @@ -147,7 +158,7 @@ def multi_agent_with_one_agent(env: DirectMARLEnv, state_as_observation: bool = Raises: AssertionError: If the environment state cannot be used as observation since it was explicitly defined - as unconstructed (:attr:`DirectMARLEnvCfg.num_states`). + as unconstructed (:attr:`DirectMARLEnvCfg.state_space`). """ class Env(DirectMARLEnv): @@ -157,7 +168,7 @@ def __init__(self, env: DirectMARLEnv) -> None: # check if it is possible to use the multi-agent environment state as agent observation self._state_as_observation = state_as_observation if self._state_as_observation: - assert self.env.cfg.num_states != 0, ( + assert self.env.cfg.state_space != 0, ( "The environment state cannot be used as observation since it was explicitly defined as" " unconstructed" ) @@ -170,13 +181,13 @@ def __init__(self, env: DirectMARLEnv) -> None: self._exported_observation_spaces = {self._agent_id: self.env.state_space} else: self._exported_observation_spaces = { - self._agent_id: gym.spaces.Box( - low=-np.inf, high=np.inf, shape=(sum(self.env.cfg.num_observations.values()),) + self._agent_id: gym.spaces.flatten_space( + gym.spaces.Tuple([self.env.observation_spaces[agent] for agent in self.env.possible_agents]) ) } self._exported_action_spaces = { - self._agent_id: gym.spaces.Box( - low=-np.inf, high=np.inf, shape=(sum(self.env.cfg.num_actions.values()),) + self._agent_id: gym.spaces.flatten_space( + gym.spaces.Tuple([self.env.action_spaces[agent] for agent in self.env.possible_agents]) ) } @@ -208,18 +219,25 @@ def reset( if self._state_as_observation: obs = {self._agent_id: self.env.state()} # concatenate agents' observations + # FIXME: This implementation assumes the spaces are fundamental ones. Fix it to support composite spaces else: - obs = {self._agent_id: torch.cat([obs[agent] for agent in self.env.possible_agents], dim=-1)} + obs = { + self._agent_id: torch.cat( + [obs[agent].reshape(self.num_envs, -1) for agent in self.env.possible_agents], dim=-1 + ) + } return obs, extras def step(self, actions: dict[AgentID, ActionType]) -> EnvStepReturn: # split agent actions to build the multi-agent ones + # FIXME: This implementation assumes the spaces are fundamental ones. Fix it to support composite spaces index = 0 _actions = {} for agent in self.env.possible_agents: - _actions[agent] = actions[self._agent_id][:, index : index + self.env.cfg.num_actions[agent]] - index += self.env.cfg.num_actions[agent] + delta = gym.spaces.flatdim(self.env.action_spaces[agent]) + _actions[agent] = actions[self._agent_id][:, index : index + delta] + index += delta # step the environment obs, rewards, terminated, time_outs, extras = self.env.step(_actions) @@ -228,8 +246,13 @@ def step(self, actions: dict[AgentID, ActionType]) -> EnvStepReturn: if self._state_as_observation: obs = {self._agent_id: self.env.state()} # concatenate agents' observations + # FIXME: This implementation assumes the spaces are fundamental ones. Fix it to support composite spaces else: - obs = {self._agent_id: torch.cat([obs[agent] for agent in self.env.possible_agents], dim=-1)} + obs = { + self._agent_id: torch.cat( + [obs[agent].reshape(self.num_envs, -1) for agent in self.env.possible_agents], dim=-1 + ) + } # process environment outputs to return agent data rewards = {self._agent_id: sum(rewards.values())} diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils/spaces.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils/spaces.py new file mode 100644 index 0000000000..2a1e30c1ee --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/envs/utils/spaces.py @@ -0,0 +1,221 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import gymnasium as gym +import json +import numpy as np +import torch +from typing import Any + +from ..common import SpaceType + + +def spec_to_gym_space(spec: SpaceType) -> gym.spaces.Space: + """Generate an appropriate Gymnasium space according to the given space specification. + + Args: + spec: Space specification. + + Returns: + Gymnasium space. + + Raises: + ValueError: If the given space specification is not valid/supported. + """ + if isinstance(spec, gym.spaces.Space): + return spec + # fundamental spaces + # Box + elif isinstance(spec, int): + return gym.spaces.Box(low=-np.inf, high=np.inf, shape=(spec,)) + elif isinstance(spec, list) and all(isinstance(x, int) for x in spec): + return gym.spaces.Box(low=-np.inf, high=np.inf, shape=spec) + # Discrete + elif isinstance(spec, set) and len(spec) == 1: + return gym.spaces.Discrete(n=next(iter(spec))) + # MultiDiscrete + elif isinstance(spec, list) and all(isinstance(x, set) and len(x) == 1 for x in spec): + return gym.spaces.MultiDiscrete(nvec=[next(iter(x)) for x in spec]) + # composite spaces + # Tuple + elif isinstance(spec, tuple): + return gym.spaces.Tuple([spec_to_gym_space(x) for x in spec]) + # Dict + elif isinstance(spec, dict): + return gym.spaces.Dict({k: spec_to_gym_space(v) for k, v in spec.items()}) + raise ValueError(f"Unsupported space specification: {spec}") + + +def sample_space(space: gym.spaces.Space, device: str, batch_size: int = -1, fill_value: float | None = None) -> Any: + """Sample a Gymnasium space where the data container are PyTorch tensors. + + Args: + space: Gymnasium space. + device: The device where the tensor should be created. + batch_size: Batch size. If the specified value is greater than zero, a batched space will be created and sampled from it. + fill_value: The value to fill the created tensors with. If None (default value), tensors will keep their random values. + + Returns: + Tensorized sampled space. + """ + + def tensorize(s, x): + if isinstance(s, gym.spaces.Box): + tensor = torch.tensor(x, device=device, dtype=torch.float32).reshape(batch_size, *s.shape) + if fill_value is not None: + tensor.fill_(fill_value) + return tensor + elif isinstance(s, gym.spaces.Discrete): + if isinstance(x, np.ndarray): + tensor = torch.tensor(x, device=device, dtype=torch.int64).reshape(batch_size, 1) + if fill_value is not None: + tensor.fill_(int(fill_value)) + return tensor + elif isinstance(x, np.number) or type(x) in [int, float]: + tensor = torch.tensor([x], device=device, dtype=torch.int64).reshape(batch_size, 1) + if fill_value is not None: + tensor.fill_(int(fill_value)) + return tensor + elif isinstance(s, gym.spaces.MultiDiscrete): + if isinstance(x, np.ndarray): + tensor = torch.tensor(x, device=device, dtype=torch.int64).reshape(batch_size, *s.shape) + if fill_value is not None: + tensor.fill_(int(fill_value)) + return tensor + elif isinstance(s, gym.spaces.Dict): + return {k: tensorize(_s, x[k]) for k, _s in s.items()} + elif isinstance(s, gym.spaces.Tuple): + return tuple([tensorize(_s, v) for _s, v in zip(s, x)]) + + sample = (gym.vector.utils.batch_space(space, batch_size) if batch_size > 0 else space).sample() + return tensorize(space, sample) + + +def serialize_space(space: SpaceType) -> str: + """Serialize a space specification as JSON. + + Args: + space: Space specification. + + Returns: + Serialized JSON representation. + """ + # Gymnasium spaces + if isinstance(space, gym.spaces.Discrete): + return json.dumps({"type": "gymnasium", "space": "Discrete", "n": int(space.n)}) + elif isinstance(space, gym.spaces.Box): + return json.dumps({ + "type": "gymnasium", + "space": "Box", + "low": space.low.tolist(), + "high": space.high.tolist(), + "shape": space.shape, + }) + elif isinstance(space, gym.spaces.MultiDiscrete): + return json.dumps({"type": "gymnasium", "space": "MultiDiscrete", "nvec": space.nvec.tolist()}) + elif isinstance(space, gym.spaces.Tuple): + return json.dumps({"type": "gymnasium", "space": "Tuple", "spaces": tuple(map(serialize_space, space.spaces))}) + elif isinstance(space, gym.spaces.Dict): + return json.dumps( + {"type": "gymnasium", "space": "Dict", "spaces": {k: serialize_space(v) for k, v in space.spaces.items()}} + ) + # Python data types + # Box + elif isinstance(space, int) or (isinstance(space, list) and all(isinstance(x, int) for x in space)): + return json.dumps({"type": "python", "space": "Box", "value": space}) + # Discrete + elif isinstance(space, set) and len(space) == 1: + return json.dumps({"type": "python", "space": "Discrete", "value": next(iter(space))}) + # MultiDiscrete + elif isinstance(space, list) and all(isinstance(x, set) and len(x) == 1 for x in space): + return json.dumps({"type": "python", "space": "MultiDiscrete", "value": [next(iter(x)) for x in space]}) + # composite spaces + # Tuple + elif isinstance(space, tuple): + return json.dumps({"type": "python", "space": "Tuple", "value": [serialize_space(x) for x in space]}) + # Dict + elif isinstance(space, dict): + return json.dumps( + {"type": "python", "space": "Dict", "value": {k: serialize_space(v) for k, v in space.items()}} + ) + raise ValueError(f"Unsupported space ({space})") + + +def deserialize_space(string: str) -> gym.spaces.Space: + """Deserialize a space specification encoded as JSON. + + Args: + string: Serialized JSON representation. + + Returns: + Space specification. + """ + obj = json.loads(string) + # Gymnasium spaces + if obj["type"] == "gymnasium": + if obj["space"] == "Discrete": + return gym.spaces.Discrete(n=obj["n"]) + elif obj["space"] == "Box": + return gym.spaces.Box(low=np.array(obj["low"]), high=np.array(obj["high"]), shape=obj["shape"]) + elif obj["space"] == "MultiDiscrete": + return gym.spaces.MultiDiscrete(nvec=np.array(obj["nvec"])) + elif obj["space"] == "Tuple": + return gym.spaces.Tuple(spaces=tuple(map(deserialize_space, obj["spaces"]))) + elif obj["space"] == "Dict": + return gym.spaces.Dict(spaces={k: deserialize_space(v) for k, v in obj["spaces"].items()}) + else: + raise ValueError(f"Unsupported space ({obj['spaces']})") + # Python data types + elif obj["type"] == "python": + if obj["space"] == "Discrete": + return {obj["value"]} + elif obj["space"] == "Box": + return obj["value"] + elif obj["space"] == "MultiDiscrete": + return [{x} for x in obj["value"]] + elif obj["space"] == "Tuple": + return tuple(map(deserialize_space, obj["value"])) + elif obj["space"] == "Dict": + return {k: deserialize_space(v) for k, v in obj["value"].items()} + else: + raise ValueError(f"Unsupported space ({obj['spaces']})") + else: + raise ValueError(f"Unsupported type ({obj['type']})") + + +def replace_env_cfg_spaces_with_strings(env_cfg: object) -> object: + """Replace spaces objects with their serialized JSON representations in an environment config. + + Args: + env_cfg: Environment config instance. + + Returns: + Environment config instance with spaces replaced if any. + """ + for attr in ["observation_space", "action_space", "state_space"]: + if hasattr(env_cfg, attr): + setattr(env_cfg, attr, serialize_space(getattr(env_cfg, attr))) + for attr in ["observation_spaces", "action_spaces"]: + if hasattr(env_cfg, attr): + setattr(env_cfg, attr, {k: serialize_space(v) for k, v in getattr(env_cfg, attr).items()}) + return env_cfg + + +def replace_strings_with_env_cfg_spaces(env_cfg: object) -> object: + """Replace spaces objects with their serialized JSON representations in an environment config. + + Args: + env_cfg: Environment config instance. + + Returns: + Environment config instance with spaces replaced if any. + """ + for attr in ["observation_space", "action_space", "state_space"]: + if hasattr(env_cfg, attr): + setattr(env_cfg, attr, deserialize_space(getattr(env_cfg, attr))) + for attr in ["observation_spaces", "action_spaces"]: + if hasattr(env_cfg, attr): + setattr(env_cfg, attr, {k: deserialize_space(v) for k, v in getattr(env_cfg, attr).items()}) + return env_cfg diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/__init__.py index d06f832b52..cf8bafe687 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/__init__.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/__init__.py @@ -23,10 +23,12 @@ ManagerTermBaseCfg, ObservationGroupCfg, ObservationTermCfg, + RecorderTermCfg, RewardTermCfg, TerminationTermCfg, ) from .observation_manager import ObservationManager +from .recorder_manager import DatasetExportMode, RecorderManager, RecorderManagerBaseCfg, RecorderTerm from .reward_manager import RewardManager from .scene_entity_cfg import SceneEntityCfg from .termination_manager import TerminationManager 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..50a717b6d4 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() @@ -393,5 +394,8 @@ def _prepare_terms(self): ) # create the action term term = term_cfg.class_type(term_cfg, self._env) + # sanity check if term is valid type + if not isinstance(term, CommandTerm): + raise TypeError(f"Returned object for the term '{term_name}' is not of type CommandType.") # add class to dict self._terms[term_name] = term 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 ed49189d6a..1d803bc75e 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 @@ -12,7 +12,7 @@ from prettytable import PrettyTable from typing import TYPE_CHECKING -import carb +import omni.log from .manager_base import ManagerBase, ManagerTermBase from .manager_term_cfg import EventTermCfg @@ -63,6 +63,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: @@ -163,7 +169,7 @@ def apply( """ # check if mode is valid if mode not in self._mode_term_names: - carb.log_warn(f"Event mode '{mode}' is not defined. Skipping event.") + omni.log.warn(f"Event mode '{mode}' is not defined. Skipping event.") return # check if mode is interval and dt is not provided if mode == "interval" and dt is None: @@ -358,11 +364,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() @@ -392,7 +393,7 @@ def _prepare_terms(self): ) if term_cfg.mode != "reset" and term_cfg.min_step_count_between_reset != 0: - carb.log_warn( + omni.log.warn( f"Event term '{term_name}' has 'min_step_count_between_reset' set to a non-zero value" " but the mode is not 'reset'. Ignoring the 'min_step_count_between_reset' value." ) 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 a11d0f6fdf..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 @@ -11,7 +11,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Any -import carb +import omni.log import omni.isaac.lab.utils.string as string_utils from omni.isaac.lab.utils import string_to_callable @@ -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. @@ -251,7 +252,7 @@ def _resolve_common_term_cfg(self, term_name: str, term_cfg: ManagerTermBaseCfg, if value.body_ids is not None: msg += f"\n\tBody names: {value.body_names} [{value.body_ids}]" # print the information - carb.log_info(msg) + omni.log.info(msg) # store the entity term_cfg.params[key] = value diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/manager_term_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/manager_term_cfg.py index f06daf1565..b3f38b308d 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/manager_term_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/manager_term_cfg.py @@ -22,6 +22,7 @@ from .action_manager import ActionTerm from .command_manager import CommandTerm from .manager_base import ManagerTermBase + from .recorder_manager import RecorderTerm @configclass @@ -51,6 +52,22 @@ class ManagerTermBaseCfg: """ +## +# Recorder manager. +## + + +@configclass +class RecorderTermCfg: + """Configuration for an recorder term.""" + + class_type: type[RecorderTerm] = MISSING + """The associated recorder term class. + + The class should inherit from :class:`omni.isaac.lab.managers.action_manager.RecorderTerm`. + """ + + ## # Action manager. ## @@ -152,9 +169,13 @@ class ObservationTermCfg(ManagerTermBaseCfg): """The clipping range for the observation after adding noise. Defaults to None, in which case no clipping is applied.""" - scale: float | None = None + scale: tuple[float, ...] | float | None = None """The scale to apply to the observation after clipping. Defaults to None, - in which case no scaling is applied (same as setting scale to :obj:`1`).""" + in which case no scaling is applied (same as setting scale to :obj:`1`). + + We leverage PyTorch broadcasting to scale the observation tensor with the provided value. If a tuple is provided, + please make sure the length of the tuple matches the dimensions of the tensor outputted from the term. + """ @configclass 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..6bc9b0374b 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 @@ -253,7 +259,7 @@ def compute_group(self, group_name: str) -> torch.Tensor | dict[str, torch.Tenso obs = term_cfg.noise.func(obs, term_cfg.noise) if term_cfg.clip: obs = obs.clip_(min=term_cfg.clip[0], max=term_cfg.clip[1]) - if term_cfg.scale: + if term_cfg.scale is not None: obs = obs.mul_(term_cfg.scale) # add value to list group_obs[name] = obs @@ -311,7 +317,7 @@ def _prepare_terms(self): else: group_cfg_items = group_cfg.__dict__.items() # iterate over all the terms in each group - for term_name, term_cfg in group_cfg.__dict__.items(): + for term_name, term_cfg in group_cfg_items: # skip non-obs settings if term_name in ["enable_corruption", "concatenate_terms"]: continue @@ -337,6 +343,23 @@ def _prepare_terms(self): obs_dims = tuple(term_cfg.func(self._env, **term_cfg.params).shape) self._group_obs_term_dim[group_name].append(obs_dims[1:]) + # if scale is set, check if single float or tuple + if term_cfg.scale is not None: + if not isinstance(term_cfg.scale, (float, int, tuple)): + raise TypeError( + f"Scale for observation term '{term_name}' in group '{group_name}'" + f" is not of type float, int or tuple. Received: '{type(term_cfg.scale)}'." + ) + if isinstance(term_cfg.scale, tuple) and len(term_cfg.scale) != obs_dims[1]: + raise ValueError( + f"Scale for observation term '{term_name}' in group '{group_name}'" + f" does not match the dimensions of the observation. Expected: {obs_dims[1]}" + f" but received: {len(term_cfg.scale)}." + ) + + # cast the scale into torch tensor + term_cfg.scale = torch.tensor(term_cfg.scale, dtype=torch.float, device=self._env.device) + # prepare modifiers for each observation if term_cfg.modifiers is not None: # initialize list of modifiers for term diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/recorder_manager.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/recorder_manager.py new file mode 100644 index 0000000000..5958e7d73c --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/recorder_manager.py @@ -0,0 +1,442 @@ +# Copyright (c) 2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Recorder manager for recording data produced from the given world.""" + +from __future__ import annotations + +import enum +import os +import torch +from collections.abc import Sequence +from prettytable import PrettyTable +from typing import TYPE_CHECKING + +from omni.isaac.lab.utils import configclass +from omni.isaac.lab.utils.datasets import EpisodeData, HDF5DatasetFileHandler + +from .manager_base import ManagerBase, ManagerTermBase +from .manager_term_cfg import RecorderTermCfg + +if TYPE_CHECKING: + from omni.isaac.lab.envs import ManagerBasedEnv + + +class DatasetExportMode(enum.IntEnum): + """The mode to handle episode exports.""" + + EXPORT_ALL = 0 # Export all episodes to a single dataset file + EXPORT_SUCCEEDED_FAILED_IN_SEPARATE_FILES = 1 # Export succeeded and failed episodes in separate files + EXPORT_SUCCEEDED_ONLY = 2 # Export only succeeded episodes to a single dataset file + + +@configclass +class RecorderManagerBaseCfg: + """Base class for configuring recorder manager terms.""" + + dataset_file_handler_class_type: type = HDF5DatasetFileHandler + + dataset_export_dir_path: str = "/tmp/isaaclab/logs" + """The directory path where the recorded datasets are exported.""" + + dataset_filename: str = "dataset" + """Dataset file name without file extension.""" + + dataset_export_mode: DatasetExportMode = DatasetExportMode.EXPORT_ALL + """The mode to handle episode exports.""" + + +class RecorderTerm(ManagerTermBase): + """Base class for recorder terms. + + The recorder term is responsible for recording data at various stages of the environment's lifecycle. + A recorder term is comprised of four user-defined callbacks to record data in the corresponding stages: + + * Pre-reset recording: This callback is invoked at the beginning of `env.reset()` before the reset is effective. + * Post-reset recording: This callback is invoked at the end of `env.reset()`. + * Pre-step recording: This callback is invoked at the beginning of `env.step()`, after the step action is processed + and before the action is applied by the action manager. + * Post-step recording: This callback is invoked at the end of `env.step()` when all the managers are processed. + """ + + def __init__(self, cfg: RecorderTermCfg, env: ManagerBasedEnv): + """Initialize the recorder term. + + Args: + cfg: The configuration object. + env: The environment instance. + """ + # call the base class constructor + super().__init__(cfg, env) + + """ + User-defined callbacks. + """ + + def record_pre_reset(self, env_ids: Sequence[int] | None) -> tuple[str | None, torch.Tensor | dict | None]: + """Record data at the beginning of env.reset() before reset is effective. + + Args: + env_ids: The environment ids. All environments should be considered when set to None. + + Returns: + A tuple of key and value to be recorded. + The key can contain nested keys separated by '/'. For example, "obs/joint_pos" would add the given + value under ['obs']['policy'] in the underlying dictionary in the recorded episode data. + The value can be a tensor or a nested dictionary of tensors. The shape of a tensor in the value + is (env_ids, ...). + """ + return None, None + + def record_post_reset(self, env_ids: Sequence[int] | None) -> tuple[str | None, torch.Tensor | dict | None]: + """Record data at the end of env.reset(). + + Args: + env_ids: The environment ids. All environments should be considered when set to None. + + Returns: + A tuple of key and value to be recorded. + Please refer to the `record_pre_reset` function for more details. + """ + return None, None + + def record_pre_step(self) -> tuple[str | None, torch.Tensor | dict | None]: + """Record data in the beginning of env.step() after action is cached/processed in the ActionManager. + + Returns: + A tuple of key and value to be recorded. + Please refer to the `record_pre_reset` function for more details. + """ + return None, None + + def record_post_step(self) -> tuple[str | None, torch.Tensor | dict | None]: + """Record data at the end of env.step() when all the managers are processed. + + Returns: + A tuple of key and value to be recorded. + Please refer to the `record_pre_reset` function for more details. + """ + return None, None + + +class RecorderManager(ManagerBase): + """Manager for recording data from recorder terms.""" + + def __init__(self, cfg: object, env: ManagerBasedEnv): + """Initialize the recorder manager. + + Args: + cfg: The configuration object or dictionary (``dict[str, RecorderTermCfg]``). + env: The environment instance. + """ + self._term_names: list[str] = list() + self._terms: dict[str, RecorderTerm] = dict() + + # Do nothing if cfg is None or an empty dict + if not cfg: + return + + super().__init__(cfg, env) + + # Do nothing if no active recorder terms are provided + if len(self.active_terms) == 0: + return + + if not isinstance(cfg, RecorderManagerBaseCfg): + raise TypeError("Configuration for the recorder manager is not of type RecorderManagerBaseCfg.") + + # create episode data buffer indexed by environment id + self._episodes: dict[int, EpisodeData] = dict() + for env_id in range(env.num_envs): + self._episodes[env_id] = EpisodeData() + + env_name = getattr(env.cfg, "env_name", None) + + self._dataset_file_handler = cfg.dataset_file_handler_class_type() + self._dataset_file_handler.create( + os.path.join(cfg.dataset_export_dir_path, cfg.dataset_filename), env_name=env_name + ) + + self._failed_episode_dataset_file_handler = None + if cfg.dataset_export_mode == DatasetExportMode.EXPORT_SUCCEEDED_FAILED_IN_SEPARATE_FILES: + self._failed_episode_dataset_file_handler = cfg.dataset_file_handler_class_type() + self._failed_episode_dataset_file_handler.create( + os.path.join(cfg.dataset_export_dir_path, f"{cfg.dataset_filename}_failed"), env_name=env_name + ) + + def __str__(self) -> str: + """Returns: A string representation for recorder manager.""" + msg = f" contains {len(self._term_names)} active terms.\n" + # create table for term information + table = PrettyTable() + table.title = "Active Recorder Terms" + table.field_names = ["Index", "Name"] + # set alignment of table columns + table.align["Name"] = "l" + # add info on each term + for index, name in enumerate(self._term_names): + table.add_row([index, name]) + # convert table to string + msg += table.get_string() + msg += "\n" + return msg + + def __del__(self): + """Destructor for recorder.""" + # Do nothing if no active recorder terms are provided + if len(self.active_terms) == 0: + return + + if self._dataset_file_handler is not None: + self._dataset_file_handler.close() + + """ + Properties. + """ + + @property + def active_terms(self) -> list[str]: + """Name of active recorder terms.""" + return self._term_names + + """ + Operations. + """ + + def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor]: + """Resets the recorder data. + + Args: + env_ids: The environment ids. Defaults to None, in which case + all environments are considered. + + Returns: + An empty dictionary. + """ + # Do nothing if no active recorder terms are provided + if len(self.active_terms) == 0: + return {} + + # resolve environment ids + if env_ids is None: + env_ids = list(range(self._env.num_envs)) + if isinstance(env_ids, torch.Tensor): + env_ids = env_ids.tolist() + + for term in self._terms.values(): + term.reset(env_ids=env_ids) + + for env_id in env_ids: + self._episodes[env_id] = EpisodeData() + + # nothing to log here + return {} + + def get_episode(self, env_id: int) -> EpisodeData: + """Returns the episode data for the given environment id. + + Args: + env_id: The environment id. + + Returns: + The episode data for the given environment id. + """ + return self._episodes.get(env_id, EpisodeData()) + + def add_to_episodes(self, key: str, value: torch.Tensor | dict, env_ids: Sequence[int] | None = None): + """Adds the given key-value pair to the episodes for the given environment ids. + + Args: + key: The key of the given value to be added to the episodes. The key can contain nested keys + separated by '/'. For example, "obs/joint_pos" would add the given value under ['obs']['policy'] + in the underlying dictionary in the episode data. + value: The value to be added to the episodes. The value can be a tensor or a nested dictionary of tensors. + The shape of a tensor in the value is (env_ids, ...). + env_ids: The environment ids. Defaults to None, in which case all environments are considered. + """ + # Do nothing if no active recorder terms are provided + if len(self.active_terms) == 0: + return + + # resolve environment ids + if key is None: + return + if env_ids is None: + env_ids = list(range(self._env.num_envs)) + if isinstance(env_ids, torch.Tensor): + env_ids = env_ids.tolist() + + if isinstance(value, dict): + for sub_key, sub_value in value.items(): + self.add_to_episodes(f"{key}/{sub_key}", sub_value, env_ids) + return + + for value_index, env_id in enumerate(env_ids): + if env_id not in self._episodes: + self._episodes[env_id] = EpisodeData() + self._episodes[env_id].env_id = env_id + self._episodes[env_id].add(key, value[value_index]) + + def set_success_to_episodes(self, env_ids: Sequence[int] | None, success_values: torch.Tensor): + """Sets the task success values to the episodes for the given environment ids. + + Args: + env_ids: The environment ids. Defaults to None, in which case all environments are considered. + success_values: The task success values to be set to the episodes. The shape of the tensor is (env_ids, 1). + """ + # Do nothing if no active recorder terms are provided + if len(self.active_terms) == 0: + return + + # resolve environment ids + if env_ids is None: + env_ids = list(range(self._env.num_envs)) + if isinstance(env_ids, torch.Tensor): + env_ids = env_ids.tolist() + + for value_index, env_id in enumerate(env_ids): + self._episodes[env_id].success = success_values[value_index].item() + + def record_pre_step(self) -> None: + """Trigger recorder terms for pre-step functions.""" + # Do nothing if no active recorder terms are provided + if len(self.active_terms) == 0: + return + + for term in self._terms.values(): + key, value = term.record_pre_step() + self.add_to_episodes(key, value) + + def record_post_step(self) -> None: + """Trigger recorder terms for post-step functions.""" + # Do nothing if no active recorder terms are provided + if len(self.active_terms) == 0: + return + + for term in self._terms.values(): + key, value = term.record_post_step() + self.add_to_episodes(key, value) + + def record_pre_reset(self, env_ids: Sequence[int] | None) -> None: + """Trigger recorder terms for pre-reset functions. + + Args: + env_ids: The environment ids in which a reset is triggered. + """ + # Do nothing if no active recorder terms are provided + if len(self.active_terms) == 0: + return + + if env_ids is None: + env_ids = list(range(self._env.num_envs)) + if isinstance(env_ids, torch.Tensor): + env_ids = env_ids.tolist() + + for term in self._terms.values(): + key, value = term.record_pre_reset(env_ids) + self.add_to_episodes(key, value, env_ids) + + # Set task success values for the relevant episodes + success_results = torch.zeros(len(env_ids), dtype=bool, device=self._env.device) + # Check success indicator from termination terms + if "success" in self._env.termination_manager.active_terms: + success_results |= self._env.termination_manager.get_term("success")[env_ids] + self.set_success_to_episodes(env_ids, success_results) + + self.export_episodes(env_ids) + + def record_post_reset(self, env_ids: Sequence[int] | None) -> None: + """Trigger recorder terms for post-reset functions. + + Args: + env_ids: The environment ids in which a reset is triggered. + """ + # Do nothing if no active recorder terms are provided + if len(self.active_terms) == 0: + return + + for term in self._terms.values(): + key, value = term.record_post_reset(env_ids) + self.add_to_episodes(key, value, env_ids) + + def export_episodes(self, env_ids: Sequence[int] | None = None) -> None: + """Concludes and exports the episodes for the given environment ids. + + Args: + env_ids: The environment ids. Defaults to None, in which case + all environments are considered. + """ + # Do nothing if no active recorder terms are provided + if len(self.active_terms) == 0: + return + + if env_ids is None: + env_ids = list(range(self._env.num_envs)) + if isinstance(env_ids, torch.Tensor): + env_ids = env_ids.tolist() + + # Export episode data through dataset exporter + need_to_flush = False + for env_id in env_ids: + if env_id in self._episodes and not self._episodes[env_id].is_empty(): + episode_succeeded = self._episodes[env_id].success + target_dataset_file_handler = None + if (self.cfg.dataset_export_mode == DatasetExportMode.EXPORT_ALL) or ( + self.cfg.dataset_export_mode == DatasetExportMode.EXPORT_SUCCEEDED_ONLY and episode_succeeded + ): + target_dataset_file_handler = self._dataset_file_handler + elif self.cfg.dataset_export_mode == DatasetExportMode.EXPORT_SUCCEEDED_FAILED_IN_SEPARATE_FILES: + if episode_succeeded: + target_dataset_file_handler = self._dataset_file_handler + else: + target_dataset_file_handler = self._failed_episode_dataset_file_handler + if target_dataset_file_handler is not None: + target_dataset_file_handler.write_episode(self._episodes[env_id]) + need_to_flush = True + # Reset the episode buffer for the given environment after export + self._episodes[env_id] = EpisodeData() + + if need_to_flush: + self._dataset_file_handler.flush() + if self._failed_episode_dataset_file_handler is not None: + self._failed_episode_dataset_file_handler.flush() + + """ + Helper functions. + """ + + def _prepare_terms(self): + """Prepares a list of recorder terms.""" + # check if config is dict already + if isinstance(self.cfg, dict): + cfg_items = self.cfg.items() + else: + cfg_items = self.cfg.__dict__.items() + for term_name, term_cfg in cfg_items: + # skip non-term settings + if term_name in [ + "dataset_file_handler_class_type", + "dataset_filename", + "dataset_export_dir_path", + "dataset_export_mode", + ]: + continue + # check if term config is None + if term_cfg is None: + continue + # check valid type + if not isinstance(term_cfg, RecorderTermCfg): + raise TypeError( + f"Configuration for the term '{term_name}' is not of type RecorderTermCfg." + f" Received: '{type(term_cfg)}'." + ) + # create the recorder term + term = term_cfg.class_type(term_cfg, self._env) + # sanity check if term is valid type + if not isinstance(term, RecorderTerm): + raise TypeError(f"Returned object for the term '{term_name}' is not of type RecorderTerm.") + # add term name and parameters + self._term_names.append(term_name) + self._terms[term_name] = term 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/scene_entity_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/scene_entity_cfg.py index 17095c6615..3b801dec19 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/scene_entity_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/managers/scene_entity_cfg.py @@ -7,7 +7,7 @@ from dataclasses import MISSING -from omni.isaac.lab.assets import Articulation, RigidObject +from omni.isaac.lab.assets import Articulation, RigidObject, RigidObjectCollection from omni.isaac.lab.scene import InteractiveScene from omni.isaac.lab.utils import configclass @@ -78,16 +78,34 @@ class for more details. manager. """ + object_collection_names: str | list[str] | None = None + """The names of the objects in the rigid object collection required by the term. Defaults to None. + + The names can be either names or a regular expression matching the object names in the collection. + + These are converted to object indices on initialization of the manager and passed to the term + function as a list of object indices under :attr:`object_collection_ids`. + """ + + object_collection_ids: list[int] | slice = slice(None) + """The indices of the objects from the rigid object collection required by the term. Defaults to slice(None), + which means all the objects in the collection. + + If :attr:`object_collection_names` is specified, this is filled in automatically on initialization of the manager. + """ + preserve_order: bool = False - """Whether to preserve indices ordering to match with that in the specified joint or body names. Defaults to False. + """Whether to preserve indices ordering to match with that in the specified joint, body, or object collection names. + Defaults to False. - If False, the ordering of the indices are sorted in ascending order (i.e. the ordering in the entity's joints - or bodies). Otherwise, the indices are preserved in the order of the specified joint and body names. + If False, the ordering of the indices are sorted in ascending order (i.e. the ordering in the entity's joints, + bodies, or object in the object collection). Otherwise, the indices are preserved in the order of the specified + joint, body, or object collection names. For more details, see the :meth:`omni.isaac.lab.utils.string.resolve_matching_names` function. .. note:: - This attribute is only used when :attr:`joint_names` or :attr:`body_names` are specified. + This attribute is only used when :attr:`joint_names`, :attr:`body_names`, or :attr:`object_collection_names` are specified. """ @@ -106,6 +124,7 @@ def resolve(self, scene: InteractiveScene): ValueError: If both ``joint_names`` and ``joint_ids`` are specified and are not consistent. ValueError: If both ``fixed_tendon_names`` and ``fixed_tendon_ids`` are specified and are not consistent. ValueError: If both ``body_names`` and ``body_ids`` are specified and are not consistent. + ValueError: If both ``object_collection_names`` and ``object_collection_ids`` are specified and are not consistent. """ # check if the entity is valid if self.name not in scene.keys(): @@ -120,6 +139,9 @@ def resolve(self, scene: InteractiveScene): # convert body names to indices based on regex self._resolve_body_names(scene) + # convert object collection names to indices based on regex + self._resolve_object_collection_names(scene) + def _resolve_joint_names(self, scene: InteractiveScene): # convert joint names to indices based on regex if self.joint_names is not None or self.joint_ids != slice(None): @@ -228,3 +250,36 @@ def _resolve_body_names(self, scene: InteractiveScene): if isinstance(self.body_ids, int): self.body_ids = [self.body_ids] self.body_names = [entity.body_names[i] for i in self.body_ids] + + def _resolve_object_collection_names(self, scene: InteractiveScene): + # convert object names to indices based on regex + if self.object_collection_names is not None or self.object_collection_ids != slice(None): + entity: RigidObjectCollection = scene[self.name] + # -- if both are not their default values, check if they are valid + if self.object_collection_names is not None and self.object_collection_ids != slice(None): + if isinstance(self.object_collection_names, str): + self.object_collection_names = [self.object_collection_names] + if isinstance(self.object_collection_ids, int): + self.object_collection_ids = [self.object_collection_ids] + object_ids, _ = entity.find_objects(self.object_collection_names, preserve_order=self.preserve_order) + object_names = [entity.object_names[i] for i in self.object_collection_ids] + if object_ids != self.object_collection_ids or object_names != self.object_collection_names: + raise ValueError( + "Both 'object_collection_names' and 'object_collection_ids' are specified, and are not" + " consistent.\n\tfrom object collection names:" + f" {self.object_collection_names} [{object_ids}]\n\tfrom object collection ids:" + f" {object_names} [{self.object_collection_ids}]\nHint: Use either 'object_collection_names' or" + " 'object_collection_ids' to avoid confusion." + ) + # -- from object names to object indices + elif self.object_collection_names is not None: + if isinstance(self.object_collection_names, str): + self.object_collection_names = [self.object_collection_names] + self.object_collection_ids, _ = entity.find_objects( + self.object_collection_names, preserve_order=self.preserve_order + ) + # -- from object indices to object names + elif self.object_collection_ids != slice(None): + if isinstance(self.object_collection_ids, int): + self.object_collection_ids = [self.object_collection_ids] + self.object_collection_names = [entity.object_names[i] for i in self.object_collection_ids] 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 c803f0e305..08be6ed979 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 @@ -22,6 +22,8 @@ DeformableObjectCfg, RigidObject, RigidObjectCfg, + RigidObjectCollection, + RigidObjectCollectionCfg, ) from omni.isaac.lab.sensors import ContactSensorCfg, FrameTransformerCfg, SensorBase, SensorBaseCfg from omni.isaac.lab.terrains import TerrainImporter, TerrainImporterCfg @@ -104,6 +106,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 @@ -111,6 +115,7 @@ def __init__(self, cfg: InteractiveSceneCfg): self._articulations = dict() self._deformable_objects = dict() self._rigid_objects = dict() + self._rigid_object_collections = dict() self._sensors = dict() self._extras = dict() # obtain the current stage @@ -166,6 +171,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: + omni.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 +204,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 +217,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 +238,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() - carb.log_info(f"Physics scene prim path: {self._physics_scene_path}") + self._physics_scene_path = prim.GetPrimPath().pathString + omni.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 @@ -296,6 +312,11 @@ def rigid_objects(self) -> dict[str, RigidObject]: """A dictionary of rigid objects in the scene.""" return self._rigid_objects + @property + def rigid_object_collections(self) -> dict[str, RigidObjectCollection]: + """A dictionary of rigid object collections in the scene.""" + return self._rigid_object_collections + @property def sensors(self) -> dict[str, SensorBase]: """A dictionary of the sensors in the scene, such as cameras and contact reporters.""" @@ -320,6 +341,56 @@ def extras(self) -> dict[str, XFormPrimView]: """ return self._extras + @property + def state(self) -> dict[str, dict[str, dict[str, torch.Tensor]]]: + """Returns the state of the scene entities. + + Returns: + A dictionary of the state of the scene entities. + """ + return self.get_state(is_relative=False) + + def get_state(self, is_relative: bool = False) -> dict[str, dict[str, dict[str, torch.Tensor]]]: + """Returns the state of the scene entities. + + Args: + is_relative: If set to True, the state is considered relative to the environment origins. + + Returns: + A dictionary of the state of the scene entities. + """ + state = dict() + # articulations + state["articulation"] = dict() + for asset_name, articulation in self._articulations.items(): + asset_state = dict() + asset_state["root_pose"] = articulation.data.root_state_w[:, :7].clone() + if is_relative: + asset_state["root_pose"][:, :3] -= self.env_origins + asset_state["root_velocity"] = articulation.data.root_vel_w.clone() + asset_state["joint_position"] = articulation.data.joint_pos.clone() + asset_state["joint_velocity"] = articulation.data.joint_vel.clone() + state["articulation"][asset_name] = asset_state + # deformable objects + state["deformable_object"] = dict() + for asset_name, deformable_object in self._deformable_objects.items(): + asset_state = dict() + asset_state["nodal_position"] = deformable_object.data.nodal_pos_w.clone() + if is_relative: + asset_state["nodal_position"][:, :3] -= self.env_origins + asset_state["nodal_velocity"] = deformable_object.data.nodal_vel_w.clone() + state["deformable_object"][asset_name] = asset_state + # rigid objects + state["rigid_object"] = dict() + for asset_name, rigid_object in self._rigid_objects.items(): + asset_state = dict() + asset_state["root_pose"] = rigid_object.data.root_state_w[:, :7].clone() + if is_relative: + asset_state["root_pose"][:, :3] -= self.env_origins + asset_state["root_velocity"] = rigid_object.data.root_vel_w.clone() + state["rigid_object"][asset_name] = asset_state + return state + """ Operations. """ @@ -338,10 +409,64 @@ def reset(self, env_ids: Sequence[int] | None = None): deformable_object.reset(env_ids) for rigid_object in self._rigid_objects.values(): rigid_object.reset(env_ids) + for rigid_object_collection in self._rigid_object_collections.values(): + rigid_object_collection.reset(env_ids) # -- sensors for sensor in self._sensors.values(): sensor.reset(env_ids) + def reset_to( + self, + state: dict[str, dict[str, dict[str, torch.Tensor]]], + env_ids: Sequence[int] | None = None, + is_relative: bool = False, + ): + """Resets the scene entities to the given state. + + Args: + state: The state to reset the scene entities to. + env_ids: The indices of the environments to reset. + Defaults to None (all instances). + is_relative: If set to True, the state is considered relative to the environment origins. + """ + if env_ids is None: + env_ids = slice(None) + # articulations + for asset_name, articulation in self._articulations.items(): + asset_state = state["articulation"][asset_name] + # root state + root_pose = asset_state["root_pose"].clone() + if is_relative: + root_pose[:, :3] += self.env_origins[env_ids] + root_velocity = asset_state["root_velocity"].clone() + articulation.write_root_pose_to_sim(root_pose, env_ids=env_ids) + articulation.write_root_velocity_to_sim(root_velocity, env_ids=env_ids) + # joint state + joint_position = asset_state["joint_position"].clone() + joint_velocity = asset_state["joint_velocity"].clone() + articulation.write_joint_state_to_sim(joint_position, joint_velocity, env_ids=env_ids) + articulation.set_joint_position_target(joint_position, env_ids=env_ids) + articulation.set_joint_velocity_target(joint_velocity, env_ids=env_ids) + # deformable objects + for asset_name, deformable_object in self._deformable_objects.items(): + asset_state = state["deformable_object"][asset_name] + nodal_position = asset_state["nodal_position"].clone() + if is_relative: + nodal_position[:, :3] += self.env_origins[env_ids] + nodal_velocity = asset_state["nodal_velocity"].clone() + deformable_object.write_nodal_pos_to_sim(nodal_position, env_ids=env_ids) + deformable_object.write_nodal_velocity_to_sim(nodal_velocity, env_ids=env_ids) + # rigid objects + for asset_name, rigid_object in self._rigid_objects.items(): + asset_state = state["rigid_object"][asset_name] + root_pose = asset_state["root_pose"].clone() + if is_relative: + root_pose[:, :3] += self.env_origins[env_ids] + root_velocity = asset_state["root_velocity"].clone() + rigid_object.write_root_pose_to_sim(root_pose, env_ids=env_ids) + rigid_object.write_root_velocity_to_sim(root_velocity, env_ids=env_ids) + self.write_data_to_sim() + def write_data_to_sim(self): """Writes the data of the scene entities to the simulation.""" # -- assets @@ -351,6 +476,8 @@ def write_data_to_sim(self): deformable_object.write_data_to_sim() for rigid_object in self._rigid_objects.values(): rigid_object.write_data_to_sim() + for rigid_object_collection in self._rigid_object_collections.values(): + rigid_object_collection.write_data_to_sim() def update(self, dt: float) -> None: """Update the scene entities. @@ -365,6 +492,8 @@ def update(self, dt: float) -> None: deformable_object.update(dt) for rigid_object in self._rigid_objects.values(): rigid_object.update(dt) + for rigid_object_collection in self._rigid_object_collections.values(): + rigid_object_collection.update(dt) # -- sensors for sensor in self._sensors.values(): sensor.update(dt, force_recompute=not self.cfg.lazy_sensor_update) @@ -384,6 +513,7 @@ def keys(self) -> list[str]: self._articulations, self._deformable_objects, self._rigid_objects, + self._rigid_object_collections, self._sensors, self._extras, ]: @@ -409,6 +539,7 @@ def __getitem__(self, key: str) -> Any: self._articulations, self._deformable_objects, self._rigid_objects, + self._rigid_object_collections, self._sensors, self._extras, ]: @@ -441,7 +572,8 @@ def _add_entities_from_cfg(self): if asset_name in InteractiveSceneCfg.__dataclass_fields__ or asset_cfg is None: continue # resolve regex - asset_cfg.prim_path = asset_cfg.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) + if hasattr(asset_cfg, "prim_path"): + asset_cfg.prim_path = asset_cfg.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) # create asset if isinstance(asset_cfg, TerrainImporterCfg): # terrains are special entities since they define environment origins @@ -454,6 +586,14 @@ def _add_entities_from_cfg(self): self._deformable_objects[asset_name] = asset_cfg.class_type(asset_cfg) elif isinstance(asset_cfg, RigidObjectCfg): self._rigid_objects[asset_name] = asset_cfg.class_type(asset_cfg) + elif isinstance(asset_cfg, RigidObjectCollectionCfg): + for rigid_object_cfg in asset_cfg.rigid_objects.values(): + rigid_object_cfg.prim_path = rigid_object_cfg.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) + self._rigid_object_collections[asset_name] = asset_cfg.class_type(asset_cfg) + for rigid_object_cfg in asset_cfg.rigid_objects.values(): + if hasattr(rigid_object_cfg, "collision_group") and rigid_object_cfg.collision_group == -1: + asset_paths = sim_utils.find_matching_prim_paths(rigid_object_cfg.prim_path) + self._global_prim_paths += asset_paths elif isinstance(asset_cfg, SensorBaseCfg): # Update target frame path(s)' regex name space for FrameTransformer if isinstance(asset_cfg, FrameTransformerCfg): diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/__init__.py index 7ec74d5047..72f1a292a6 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/__init__.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/__init__.py @@ -30,12 +30,15 @@ +---------------------+---------------------------+---------------------------------------------------------------+ | Frame Transformer | /World/robot/base | Leaf exists and is a physics body (Articulation / Rigid Body) | +---------------------+---------------------------+---------------------------------------------------------------+ +| Imu | /World/robot/base | Leaf exists and is a physics body (Rigid Body) | ++---------------------+---------------------------+---------------------------------------------------------------+ """ from .camera import * # noqa: F401, F403 from .contact_sensor import * # noqa: F401, F403 from .frame_transformer import * # noqa: F401 +from .imu import * # noqa: F401, F403 from .ray_caster import * # noqa: F401, F403 from .sensor_base import SensorBase # noqa: F401 from .sensor_base_cfg import SensorBaseCfg # noqa: F401 diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera.py index e5692887d3..091a8c1e6e 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera.py @@ -9,10 +9,10 @@ import re import torch from collections.abc import Sequence -from tensordict import TensorDict from typing import TYPE_CHECKING, Any, Literal import carb +import omni.isaac.core.utils.stage as stage_utils import omni.kit.commands import omni.usd from omni.isaac.core.prims import XFormPrimView @@ -21,11 +21,14 @@ import omni.isaac.lab.sim as sim_utils from omni.isaac.lab.utils import to_camel_case from omni.isaac.lab.utils.array import convert_to_torch -from omni.isaac.lab.utils.math import quat_from_matrix +from omni.isaac.lab.utils.math import ( + convert_camera_frame_orientation_convention, + create_rotation_matrix_from_view, + quat_from_matrix, +) from ..sensor_base import SensorBase from .camera_data import CameraData -from .utils import convert_orientation_convention, create_rotation_matrix_from_view if TYPE_CHECKING: from .camera_cfg import CameraCfg @@ -116,7 +119,9 @@ def __init__(self, cfg: CameraCfg): if self.cfg.spawn is not None: # compute the rotation offset rot = torch.tensor(self.cfg.offset.rot, dtype=torch.float32).unsqueeze(0) - rot_offset = convert_orientation_convention(rot, origin=self.cfg.offset.convention, target="opengl") + rot_offset = convert_camera_frame_orientation_convention( + rot, origin=self.cfg.offset.convention, target="opengl" + ) rot_offset = rot_offset.squeeze(0).numpy() # ensure vertical aperture is set, otherwise replace with default for squared pixels if self.cfg.spawn.vertical_aperture is None: @@ -150,7 +155,7 @@ def __str__(self) -> str: # message for class return ( f"Camera @ '{self.cfg.prim_path}': \n" - f"\tdata types : {self.data.output.sorted_keys} \n" + f"\tdata types : {list(self.data.output.keys())} \n" f"\tsemantic filter : {self.cfg.semantic_filter}\n" f"\tcolorize semantic segm. : {self.cfg.colorize_semantic_segmentation}\n" f"\tcolorize instance segm. : {self.cfg.colorize_instance_segmentation}\n" @@ -251,7 +256,7 @@ def set_intrinsic_matrices( # TODO: Adjust to handle aperture offsets once supported by omniverse # Internal ticket from rendering team: OM-42611 if params["horizontal_aperture_offset"] > 1e-4 or params["vertical_aperture_offset"] > 1e-4: - carb.log_warn("Camera aperture offsets are not supported by Omniverse. These parameters are ignored.") + omni.log.warn("Camera aperture offsets are not supported by Omniverse. These parameters are ignored.") # change data for corresponding camera index sensor_prim = self._sensor_prims[i] @@ -289,7 +294,7 @@ def set_world_poses( - :obj:`"ros"` - forward axis: +Z - up axis -Y - Offset is applied in the ROS convention - :obj:`"world"` - forward axis: +X - up axis +Z - Offset is applied in the World Frame convention - See :meth:`omni.isaac.lab.sensors.camera.utils.convert_orientation_convention` for more details + See :meth:`omni.isaac.lab.sensors.camera.utils.convert_camera_frame_orientation_convention` for more details on the conventions. Args: @@ -318,7 +323,7 @@ def set_world_poses( orientations = torch.from_numpy(orientations).to(device=self._device) elif not isinstance(orientations, torch.Tensor): orientations = torch.tensor(orientations, device=self._device) - orientations = convert_orientation_convention(orientations, origin=convention, target="opengl") + orientations = convert_camera_frame_orientation_convention(orientations, origin=convention, target="opengl") # set the pose self._view.set_world_poses(positions, orientations, env_ids) @@ -339,8 +344,10 @@ def set_world_poses_from_view( # resolve env_ids if env_ids is None: env_ids = self._ALL_INDICES + # get up axis of current stage + up_axis = stage_utils.get_stage_up_axis() # set camera poses using the view - orientations = quat_from_matrix(create_rotation_matrix_from_view(eyes, targets, device=self._device)) + orientations = quat_from_matrix(create_rotation_matrix_from_view(eyes, targets, up_axis, device=self._device)) self._view.set_world_poses(eyes, orientations, env_ids) """ @@ -489,7 +496,7 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): self._update_poses(env_ids) # -- read the data from annotator registry # check if buffer is called for the first time. If so then, allocate the memory - if len(self._data.output.sorted_keys) == 0: + if len(self._data.output) == 0: # this is the first time buffer is called # it allocates memory for all the sensors self._create_annotator_data() @@ -506,6 +513,19 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): self._data.output[name][index] = data # add info to output self._data.info[index][name] = info + # NOTE: The `distance_to_camera` annotator returns the distance to the camera optical center. However, + # the replicator depth clipping is applied w.r.t. to the image plane which may result in values + # larger than the clipping range in the output. We apply an additional clipping to ensure values + # are within the clipping range for all the annotators. + if name == "distance_to_camera": + self._data.output[name][self._data.output[name] > self.cfg.spawn.clipping_range[1]] = torch.inf + # apply defined clipping behavior + if ( + name == "distance_to_camera" or name == "distance_to_image_plane" + ) and self.cfg.depth_clipping_behavior != "none": + self._data.output[name][torch.isinf(self._data.output[name])] = ( + 0.0 if self.cfg.depth_clipping_behavior == "zero" else self.cfg.spawn.clipping_range[1] + ) """ Private Helpers @@ -544,7 +564,7 @@ def _create_buffers(self): # lazy allocation of data dictionary # since the size of the output data is not known in advance, we leave it as None # the memory will be allocated when the buffer() function is called for the first time. - self._data.output = TensorDict({}, batch_size=self._view.count, device=self.device) + self._data.output = {} self._data.info = [{name: None for name in self.cfg.data_types} for _ in range(self._view.count)] def _update_intrinsic_matrices(self, env_ids: Sequence[int]): @@ -596,7 +616,9 @@ def _update_poses(self, env_ids: Sequence[int]): # get the poses from the view poses, quat = self._view.get_world_poses(env_ids) self._data.pos_w[env_ids] = poses - self._data.quat_w_world[env_ids] = convert_orientation_convention(quat, origin="opengl", target="world") + self._data.quat_w_world[env_ids] = convert_camera_frame_orientation_convention( + quat, origin="opengl", target="world" + ) def _create_annotator_data(self): """Create the buffers to store the annotator data. @@ -622,6 +644,19 @@ def _create_annotator_data(self): self._data.info[index][name] = info # concatenate the data along the batch dimension self._data.output[name] = torch.stack(data_all_cameras, dim=0) + # NOTE: `distance_to_camera` and `distance_to_image_plane` are not both clipped to the maximum defined + # in the clipping range. The clipping is applied only to `distance_to_image_plane` and then both + # outputs are only clipped where the values in `distance_to_image_plane` exceed the threshold. To + # have a unified behavior between all cameras, we clip both outputs to the maximum value defined. + if name == "distance_to_camera": + self._data.output[name][self._data.output[name] > self.cfg.spawn.clipping_range[1]] = torch.inf + # clip the data if needed + if ( + name == "distance_to_camera" or name == "distance_to_image_plane" + ) and self.cfg.depth_clipping_behavior != "none": + self._data.output[name][torch.isinf(self._data.output[name])] = ( + 0.0 if self.cfg.depth_clipping_behavior == "zero" else self.cfg.spawn.clipping_range[1] + ) def _process_annotator_output(self, name: str, output: Any) -> tuple[torch.tensor, dict | None]: """Process the annotator output. diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera_cfg.py index ca8367224f..18052d4d16 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera_cfg.py @@ -53,6 +53,14 @@ class OffsetCfg: asset is already present in the scene. """ + depth_clipping_behavior: Literal["max", "zero", "none"] = "zero" + """Clipping behavior for the camera for values exceed the maximum value. Defaults to "zero". + + - ``"max"``: Values are clipped to the maximum value. + - ``"zero"``: Values are clipped to zero. + - ``"none``: No clipping is applied. Values will be returned as ``inf``. + """ + data_types: list[str] = ["rgb"] """List of sensor names/types to enable for the camera. Defaults to ["rgb"]. diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera_data.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera_data.py index d5f25f9e60..540fdb7dcf 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera_data.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera_data.py @@ -5,10 +5,9 @@ import torch from dataclasses import dataclass -from tensordict import TensorDict from typing import Any -from .utils import convert_orientation_convention +from omni.isaac.lab.utils.math import convert_camera_frame_orientation_convention @dataclass @@ -47,7 +46,7 @@ class CameraData: Shape is (N, 3, 3) where N is the number of sensors. """ - output: TensorDict = None + output: dict[str, torch.Tensor] = None """The retrieved sensor data with sensor types as key. The format of the data is available in the `Replicator Documentation`_. For semantic-based data, @@ -77,7 +76,7 @@ def quat_w_ros(self) -> torch.Tensor: Shape is (N, 4) where N is the number of sensors. """ - return convert_orientation_convention(self.quat_w_world, origin="world", target="ros") + return convert_camera_frame_orientation_convention(self.quat_w_world, origin="world", target="ros") @property def quat_w_opengl(self) -> torch.Tensor: @@ -89,4 +88,4 @@ def quat_w_opengl(self) -> torch.Tensor: Shape is (N, 4) where N is the number of sensors. """ - return convert_orientation_convention(self.quat_w_world, origin="world", target="opengl") + return convert_camera_frame_orientation_convention(self.quat_w_world, origin="world", target="opengl") diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/tiled_camera.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/tiled_camera.py index 1e465a4867..b9cd0efbae 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/tiled_camera.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/tiled_camera.py @@ -9,7 +9,6 @@ import numpy as np import torch from collections.abc import Sequence -from tensordict import TensorDict from typing import TYPE_CHECKING, Any import carb @@ -106,7 +105,7 @@ def __str__(self) -> str: # message for class return ( f"Tiled Camera @ '{self.cfg.prim_path}': \n" - f"\tdata types : {self.data.output.sorted_keys} \n" + f"\tdata types : {list(self.data.output.keys())} \n" f"\tsemantic filter : {self.cfg.semantic_filter}\n" f"\tcolorize semantic segm. : {self.cfg.colorize_semantic_segmentation}\n" f"\tcolorize instance segm. : {self.cfg.colorize_instance_segmentation}\n" @@ -280,6 +279,22 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): if data_type == "rgba" and "rgb" in self.cfg.data_types: self._data.output["rgb"] = self._data.output["rgba"][..., :3] + # NOTE: The `distance_to_camera` annotator returns the distance to the camera optical center. However, + # the replicator depth clipping is applied w.r.t. to the image plane which may result in values + # larger than the clipping range in the output. We apply an additional clipping to ensure values + # are within the clipping range for all the annotators. + if data_type == "distance_to_camera": + self._data.output[data_type][ + self._data.output[data_type] > self.cfg.spawn.clipping_range[1] + ] = torch.inf + # apply defined clipping behavior + if ( + data_type == "distance_to_camera" or data_type == "distance_to_image_plane" or data_type == "depth" + ) and self.cfg.depth_clipping_behavior != "none": + self._data.output[data_type][torch.isinf(self._data.output[data_type])] = ( + 0.0 if self.cfg.depth_clipping_behavior == "zero" else self.cfg.spawn.clipping_range[1] + ) + """ Private Helpers """ @@ -372,7 +387,7 @@ def _create_buffers(self): (self._view.count, self.cfg.height, self.cfg.width, 1), device=self.device, dtype=torch.int32 ).contiguous() - self._data.output = TensorDict(data_dict, batch_size=self._view.count, device=self.device) + self._data.output = data_dict self._data.info = dict() def _tiled_image_shape(self) -> tuple[int, int]: diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/utils.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/utils.py index 4e8c4e63c1..f15f537e7f 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/utils.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/utils.py @@ -8,16 +8,11 @@ # needed to import for allowing type-hinting: torch.device | str | None from __future__ import annotations -import math import numpy as np import torch -import torch.nn.functional as F from collections.abc import Sequence -from typing import Literal -import omni.isaac.core.utils.stage as stage_utils import warp as wp -from pxr import UsdGeom import omni.isaac.lab.utils.math as math_utils from omni.isaac.lab.utils.array import TensorData, convert_to_torch @@ -262,143 +257,6 @@ def create_pointcloud_from_rgbd( return points_xyz, points_rgb -def convert_orientation_convention( - orientation: torch.Tensor, - origin: Literal["opengl", "ros", "world"] = "opengl", - target: Literal["opengl", "ros", "world"] = "ros", -) -> torch.Tensor: - r"""Converts a quaternion representing a rotation from one convention to another. - - In USD, the camera follows the ``"opengl"`` convention. Thus, it is always in **Y up** convention. - This means that the camera is looking down the -Z axis with the +Y axis pointing up , and +X axis pointing right. - However, in ROS, the camera is looking down the +Z axis with the +Y axis pointing down, and +X axis pointing right. - Thus, the camera needs to be rotated by :math:`180^{\circ}` around the X axis to follow the ROS convention. - - .. math:: - - T_{ROS} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & -1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T_{USD} - - On the other hand, the typical world coordinate system is with +X pointing forward, +Y pointing left, - and +Z pointing up. The camera can also be set in this convention by rotating the camera by :math:`90^{\circ}` - around the X axis and :math:`-90^{\circ}` around the Y axis. - - .. math:: - - T_{WORLD} = \begin{bmatrix} 0 & 0 & -1 & 0 \\ -1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T_{USD} - - Thus, based on their application, cameras follow different conventions for their orientation. This function - converts a quaternion from one convention to another. - - Possible conventions are: - - - :obj:`"opengl"` - forward axis: -Z - up axis +Y - Offset is applied in the OpenGL (Usd.Camera) convention - - :obj:`"ros"` - forward axis: +Z - up axis -Y - Offset is applied in the ROS convention - - :obj:`"world"` - forward axis: +X - up axis +Z - Offset is applied in the World Frame convention - - Args: - orientation: Quaternion of form `(w, x, y, z)` with shape (..., 4) in source convention - origin: Convention to convert to. Defaults to "ros". - target: Convention to convert from. Defaults to "opengl". - - Returns: - Quaternion of form `(w, x, y, z)` with shape (..., 4) in target convention - """ - if target == origin: - return orientation.clone() - - # -- unify input type - if origin == "ros": - # convert from ros to opengl convention - rotm = math_utils.matrix_from_quat(orientation) - rotm[:, :, 2] = -rotm[:, :, 2] - rotm[:, :, 1] = -rotm[:, :, 1] - # convert to opengl convention - quat_gl = math_utils.quat_from_matrix(rotm) - elif origin == "world": - # convert from world (x forward and z up) to opengl convention - rotm = math_utils.matrix_from_quat(orientation) - rotm = torch.matmul( - rotm, - math_utils.matrix_from_euler( - torch.tensor([math.pi / 2, -math.pi / 2, 0], device=orientation.device), "XYZ" - ), - ) - # convert to isaac-sim convention - quat_gl = math_utils.quat_from_matrix(rotm) - else: - quat_gl = orientation - - # -- convert to target convention - if target == "ros": - # convert from opengl to ros convention - rotm = math_utils.matrix_from_quat(quat_gl) - rotm[:, :, 2] = -rotm[:, :, 2] - rotm[:, :, 1] = -rotm[:, :, 1] - return math_utils.quat_from_matrix(rotm) - elif target == "world": - # convert from opengl to world (x forward and z up) convention - rotm = math_utils.matrix_from_quat(quat_gl) - rotm = torch.matmul( - rotm, - math_utils.matrix_from_euler( - torch.tensor([math.pi / 2, -math.pi / 2, 0], device=orientation.device), "XYZ" - ).T, - ) - return math_utils.quat_from_matrix(rotm) - else: - return quat_gl.clone() - - -# @torch.jit.script -def create_rotation_matrix_from_view( - eyes: torch.Tensor, - targets: torch.Tensor, - device: str = "cpu", -) -> torch.Tensor: - """ - This function takes a vector ''eyes'' which specifies the location - of the camera in world coordinates and the vector ''targets'' which - indicate the position of the object. - The output is a rotation matrix representing the transformation - from world coordinates -> view coordinates. - - The inputs camera_position and targets can each be a - - 3 element tuple/list - - torch tensor of shape (1, 3) - - torch tensor of shape (N, 3) - - Args: - eyes: position of the camera in world coordinates - targets: position of the object in world coordinates - - The vectors are broadcast against each other so they all have shape (N, 3). - - Returns: - R: (N, 3, 3) batched rotation matrices - - Reference: - Based on PyTorch3D (https://github.com/facebookresearch/pytorch3d/blob/eaf0709d6af0025fe94d1ee7cec454bc3054826a/pytorch3d/renderer/cameras.py#L1635-L1685) - """ - up_axis_token = stage_utils.get_stage_up_axis() - if up_axis_token == UsdGeom.Tokens.y: - up_axis = torch.tensor((0, 1, 0), device=device, dtype=torch.float32).repeat(eyes.shape[0], 1) - elif up_axis_token == UsdGeom.Tokens.z: - up_axis = torch.tensor((0, 0, 1), device=device, dtype=torch.float32).repeat(eyes.shape[0], 1) - else: - raise ValueError(f"Invalid up axis: {up_axis_token}") - - # get rotation matrix in opengl format (-Z forward, +Y up) - z_axis = -F.normalize(targets - eyes, eps=1e-5) - x_axis = F.normalize(torch.cross(up_axis, z_axis, dim=1), eps=1e-5) - y_axis = F.normalize(torch.cross(z_axis, x_axis, dim=1), eps=1e-5) - is_close = torch.isclose(x_axis, torch.tensor(0.0), atol=5e-3).all(dim=1, keepdim=True) - if is_close.any(): - replacement = F.normalize(torch.cross(y_axis, z_axis, dim=1), eps=1e-5) - x_axis = torch.where(is_close, replacement, x_axis) - R = torch.cat((x_axis[:, None, :], y_axis[:, None, :], z_axis[:, None, :]), dim=1) - return R.transpose(1, 2) - - def save_images_to_file(images: torch.Tensor, file_path: str): """Save images to file. diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/frame_transformer/frame_transformer.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/frame_transformer/frame_transformer.py index 87205cf4cd..62f52ea345 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/frame_transformer/frame_transformer.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/frame_transformer/frame_transformer.py @@ -10,7 +10,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -import carb +import omni.log import omni.physics.tensors.impl.api as physx from pxr import UsdPhysics @@ -114,10 +114,10 @@ def _initialize_impl(self): self._apply_source_frame_offset = True # Handle source frame offsets if is_identity_pose(source_frame_offset_pos, source_frame_offset_quat): - carb.log_verbose(f"No offset application needed for source frame as it is identity: {self.cfg.prim_path}") + omni.log.verbose(f"No offset application needed for source frame as it is identity: {self.cfg.prim_path}") self._apply_source_frame_offset = False else: - carb.log_verbose(f"Applying offset to source frame as it is not identity: {self.cfg.prim_path}") + omni.log.verbose(f"Applying offset to source frame as it is not identity: {self.cfg.prim_path}") # Store offsets as tensors (duplicating each env's offsets for ease of multiplication later) self._source_frame_offset_pos = source_frame_offset_pos.unsqueeze(0).repeat(self._num_envs, 1) self._source_frame_offset_quat = source_frame_offset_quat.unsqueeze(0).repeat(self._num_envs, 1) @@ -194,12 +194,12 @@ def _initialize_impl(self): target_offsets[frame_name] = {"pos": offset_pos, "quat": offset_quat} if not self._apply_target_frame_offset: - carb.log_info( + omni.log.info( f"No offsets application needed from '{self.cfg.prim_path}' to target frames as all" f" are identity: {frames[1:]}" ) else: - carb.log_info( + omni.log.info( f"Offsets application needed from '{self.cfg.prim_path}' to the following target frames:" f" {non_identity_offset_frames}" ) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/imu/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/imu/__init__.py new file mode 100644 index 0000000000..c1d2015e85 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/imu/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Imu Sensor +""" + +from .imu import Imu +from .imu_cfg import ImuCfg +from .imu_data import ImuData diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/imu/imu.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/imu/imu.py new file mode 100644 index 0000000000..feb434a81f --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/imu/imu.py @@ -0,0 +1,243 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import omni.isaac.core.utils.stage as stage_utils +import omni.physics.tensors.impl.api as physx +from pxr import UsdPhysics + +import omni.isaac.lab.sim as sim_utils +import omni.isaac.lab.utils.math as math_utils +from omni.isaac.lab.markers import VisualizationMarkers + +from ..sensor_base import SensorBase +from .imu_data import ImuData + +if TYPE_CHECKING: + from .imu_cfg import ImuCfg + + +class Imu(SensorBase): + """The Inertia Measurement Unit (IMU) sensor. + + The sensor can be attached to any :class:`RigidObject` or :class:`Articulation` in the scene. The sensor provides complete state information. + The sensor is primarily used to provide the linear acceleration and angular velocity of the object in the body frame. The sensor also provides + the position and orientation of the object in the world frame and the angular acceleration and linear velocity in the body frame. The extra + data outputs are useful for simulating with or comparing against "perfect" state estimation. + + .. note:: + + We are computing the accelerations using numerical differentiation from the velocities. Consequently, the + IMU sensor accuracy depends on the chosen phsyx timestep. For a sufficient accuracy, we recommend to keep the + timestep at least as 200Hz. + + .. note:: + + It is suggested to use the OffsetCfg to define an IMU frame relative to a rigid body prim defined at the root of + a :class:`RigidObject` or a prim that is defined by a non-fixed joint in an :class:`Articulation` (except for the + root of a fixed based articulation). The use frames with fixed joints and small mass/inertia to emulate a transform + relative to a body frame can result in lower performance and accuracy. + + """ + + cfg: ImuCfg + """The configuration parameters.""" + + def __init__(self, cfg: ImuCfg): + """Initializes the Imu sensor. + + Args: + cfg: The configuration parameters. + """ + # initialize base class + super().__init__(cfg) + # Create empty variables for storing output data + self._data = ImuData() + + def __str__(self) -> str: + """Returns: A string containing information about the instance.""" + return ( + f"Imu sensor @ '{self.cfg.prim_path}': \n" + f"\tview type : {self._view.__class__}\n" + f"\tupdate period (s) : {self.cfg.update_period}\n" + f"\tnumber of sensors : {self._view.count}\n" + ) + + """ + Properties + """ + + @property + def data(self) -> ImuData: + # update sensors if needed + self._update_outdated_buffers() + # return the data + return self._data + + @property + def num_instances(self) -> int: + return self._view.count + + """ + Operations + """ + + def reset(self, env_ids: Sequence[int] | None = None): + # reset the timestamps + super().reset(env_ids) + # resolve None + if env_ids is None: + env_ids = slice(None) + # reset accumulative data buffers + self._data.quat_w[env_ids] = 0.0 + self._data.lin_vel_b[env_ids] = 0.0 + self._data.ang_vel_b[env_ids] = 0.0 + self._data.lin_acc_b[env_ids] = 0.0 + self._data.ang_acc_b[env_ids] = 0.0 + + def update(self, dt: float, force_recompute: bool = False): + # save timestamp + self._dt = dt + # execute updating + super().update(dt, force_recompute) + + """ + Implementation. + """ + + def _initialize_impl(self): + """Initializes the sensor handles and internal buffers. + + This function creates handles and registers the provided data types with the replicator registry to + be able to access the data from the sensor. It also initializes the internal buffers to store the data. + + Raises: + RuntimeError: If the imu prim is not a RigidBodyPrim + """ + # Initialize parent class + super()._initialize_impl() + # create simulation view + self._physics_sim_view = physx.create_simulation_view(self._backend) + self._physics_sim_view.set_subspace_roots("/") + # check if the prim at path is a rigid prim + prim = sim_utils.find_first_matching_prim(self.cfg.prim_path) + if prim is None: + raise RuntimeError(f"Failed to find a prim at path expression: {self.cfg.prim_path}") + # check if it is a RigidBody Prim + if prim.HasAPI(UsdPhysics.RigidBodyAPI): + self._view = self._physics_sim_view.create_rigid_body_view(self.cfg.prim_path.replace(".*", "*")) + else: + raise RuntimeError(f"Failed to find a RigidBodyAPI for the prim paths: {self.cfg.prim_path}") + + # Create internal buffers + self._initialize_buffers_impl() + + def _update_buffers_impl(self, env_ids: Sequence[int]): + """Fills the buffers of the sensor data.""" + # check if self._dt is set (this is set in the update function) + if not hasattr(self, "_dt"): + raise RuntimeError( + "The update function must be called before the data buffers are accessed the first time." + ) + # default to all sensors + if len(env_ids) == self._num_envs: + env_ids = slice(None) + # obtain the poses of the sensors + pos_w, quat_w = self._view.get_transforms()[env_ids].split([3, 4], dim=-1) + quat_w = math_utils.convert_quat(quat_w, to="wxyz") + + # store the poses + self._data.pos_w[env_ids] = pos_w + math_utils.quat_rotate(quat_w, self._offset_pos_b[env_ids]) + self._data.quat_w[env_ids] = math_utils.quat_mul(quat_w, self._offset_quat_b[env_ids]) + + # get the offset from COM to link origin + com_pos_b = self._view.get_coms().to(self.device).split([3, 4], dim=-1)[0] + + # obtain the velocities of the link COM + lin_vel_w, ang_vel_w = self._view.get_velocities()[env_ids].split([3, 3], dim=-1) + # if an offset is present or the COM does not agree with the link origin, the linear velocity has to be + # transformed taking the angular velocity into account + lin_vel_w += torch.linalg.cross( + ang_vel_w, math_utils.quat_rotate(quat_w, self._offset_pos_b[env_ids] - com_pos_b[env_ids]), dim=-1 + ) + + # numerical derivative + lin_acc_w = (lin_vel_w - self._prev_lin_vel_w[env_ids]) / self._dt + self._gravity_bias_w[env_ids] + ang_acc_w = (ang_vel_w - self._prev_ang_vel_w[env_ids]) / self._dt + # store the velocities + self._data.lin_vel_b[env_ids] = math_utils.quat_rotate_inverse(self._data.quat_w[env_ids], lin_vel_w) + self._data.ang_vel_b[env_ids] = math_utils.quat_rotate_inverse(self._data.quat_w[env_ids], ang_vel_w) + # store the accelerations + self._data.lin_acc_b[env_ids] = math_utils.quat_rotate_inverse(self._data.quat_w[env_ids], lin_acc_w) + self._data.ang_acc_b[env_ids] = math_utils.quat_rotate_inverse(self._data.quat_w[env_ids], ang_acc_w) + + self._prev_lin_vel_w[env_ids] = lin_vel_w + self._prev_ang_vel_w[env_ids] = ang_vel_w + + def _initialize_buffers_impl(self): + """Create buffers for storing data.""" + # data buffers + self._data.pos_w = torch.zeros(self._view.count, 3, device=self._device) + self._data.quat_w = torch.zeros(self._view.count, 4, device=self._device) + self._data.quat_w[:, 0] = 1.0 + self._data.lin_vel_b = torch.zeros_like(self._data.pos_w) + self._data.ang_vel_b = torch.zeros_like(self._data.pos_w) + self._data.lin_acc_b = torch.zeros_like(self._data.pos_w) + self._data.ang_acc_b = torch.zeros_like(self._data.pos_w) + self._prev_lin_vel_w = torch.zeros_like(self._data.pos_w) + self._prev_ang_vel_w = torch.zeros_like(self._data.pos_w) + + # store sensor offset transformation + self._offset_pos_b = torch.tensor(list(self.cfg.offset.pos), device=self._device).repeat(self._view.count, 1) + self._offset_quat_b = torch.tensor(list(self.cfg.offset.rot), device=self._device).repeat(self._view.count, 1) + # set gravity bias + self._gravity_bias_w = torch.tensor(list(self.cfg.gravity_bias), device=self._device).repeat( + self._view.count, 1 + ) + + def _set_debug_vis_impl(self, debug_vis: bool): + # set visibility of markers + # note: parent only deals with callbacks. not their visibility + if debug_vis: + # create markers if necessary for the first tome + if not hasattr(self, "acceleration_visualizer"): + self.acceleration_visualizer = VisualizationMarkers(self.cfg.visualizer_cfg) + # set their visibility to true + self.acceleration_visualizer.set_visibility(True) + else: + if hasattr(self, "acceleration_visualizer"): + self.acceleration_visualizer.set_visibility(False) + + def _debug_vis_callback(self, event): + # safely return if view becomes invalid + # note: this invalidity happens because of isaac sim view callbacks + if self._view is None: + return + # get marker location + # -- base state + base_pos_w = self._data.pos_w.clone() + base_pos_w[:, 2] += 0.5 + # -- resolve the scales + default_scale = self.acceleration_visualizer.cfg.markers["arrow"].scale + arrow_scale = torch.tensor(default_scale, device=self.device).repeat(self._data.lin_acc_b.shape[0], 1) + # get up axis of current stage + up_axis = stage_utils.get_stage_up_axis() + # arrow-direction + quat_opengl = math_utils.quat_from_matrix( + math_utils.create_rotation_matrix_from_view( + self._data.pos_w, + self._data.pos_w + math_utils.quat_rotate(self._data.quat_w, self._data.lin_acc_b), + up_axis=up_axis, + device=self._device, + ) + ) + quat_w = math_utils.convert_camera_frame_orientation_convention(quat_opengl, "opengl", "world") + # display markers + self.acceleration_visualizer.visualize(base_pos_w, quat_w, arrow_scale) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/imu/imu_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/imu/imu_cfg.py new file mode 100644 index 0000000000..74e38f9e6e --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/imu/imu_cfg.py @@ -0,0 +1,46 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from omni.isaac.lab.markers import VisualizationMarkersCfg +from omni.isaac.lab.markers.config import RED_ARROW_X_MARKER_CFG +from omni.isaac.lab.utils import configclass + +from ..sensor_base_cfg import SensorBaseCfg +from .imu import Imu + + +@configclass +class ImuCfg(SensorBaseCfg): + """Configuration for an Inertial Measurement Unit (IMU) sensor.""" + + class_type: type = Imu + + @configclass + class OffsetCfg: + """The offset pose of the sensor's frame from the sensor's parent frame.""" + + pos: tuple[float, float, float] = (0.0, 0.0, 0.0) + """Translation w.r.t. the parent frame. Defaults to (0.0, 0.0, 0.0).""" + + rot: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) + """Quaternion rotation (w, x, y, z) w.r.t. the parent frame. Defaults to (1.0, 0.0, 0.0, 0.0).""" + + offset: OffsetCfg = OffsetCfg() + """The offset pose of the sensor's frame from the sensor's parent frame. Defaults to identity.""" + + visualizer_cfg: VisualizationMarkersCfg = RED_ARROW_X_MARKER_CFG.replace(prim_path="/Visuals/Command/velocity_goal") + """The configuration object for the visualization markers. Defaults to RED_ARROW_X_MARKER_CFG. + + This attribute is only used when debug visualization is enabled. + """ + gravity_bias: tuple[float, float, float] = (0.0, 0.0, 9.81) + """The linear acceleration bias applied to the linear acceleration in the world frame (x,y,z). + + Imu sensors typically output a positive gravity acceleration in opposition to the direction of gravity. This + config parameter allows users to subtract that bias if set to (0.,0.,0.). By default this is set to (0.0,0.0,9.81) + which results in a positive acceleration reading in the world Z. + """ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/imu/imu_data.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/imu/imu_data.py new file mode 100644 index 0000000000..37920cd841 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/imu/imu_data.py @@ -0,0 +1,50 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from dataclasses import dataclass + + +@dataclass +class ImuData: + """Data container for the Imu sensor.""" + + pos_w: torch.Tensor = None + """Position of the sensor origin in world frame. + + Shape is (N, 3), where ``N`` is the number of environments. + """ + + quat_w: torch.Tensor = None + """Orientation of the sensor origin in quaternion ``(w, x, y, z)`` in world frame. + + Shape is (N, 4), where ``N`` is the number of environments. + """ + + lin_vel_b: torch.Tensor = None + """IMU frame angular velocity relative to the world expressed in IMU frame. + + Shape is (N, 3), where ``N`` is the number of environments. + """ + + ang_vel_b: torch.Tensor = None + """IMU frame angular velocity relative to the world expressed in IMU frame. + + Shape is (N, 3), where ``N`` is the number of environments. + """ + + lin_acc_b: torch.Tensor = None + """IMU frame linear acceleration relative to the world expressed in IMU frame. + + Shape is (N, 3), where ``N`` is the number of environments. + """ + + ang_acc_b: torch.Tensor = None + """IMU frame angular acceleration relative to the world expressed in IMU frame. + + Shape is (N, 3), where ``N`` is the number of environments. + """ 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 d4ad21b004..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,9 +9,9 @@ import re import torch from collections.abc import Sequence -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING -import carb +import omni.log import omni.physics.tensors.impl.api as physx import warp as wp from omni.isaac.core.prims import XFormPrimView @@ -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}" @@ -144,7 +138,7 @@ def _initialize_impl(self): else: self._view = XFormPrimView(self.cfg.prim_path, reset_xform_properties=False) found_supported_prim_class = True - carb.log_warn(f"The prim at path {prim.GetPath().pathString} is not a physics prim! Using XFormPrimView.") + omni.log.warn(f"The prim at path {prim.GetPath().pathString} is not a physics prim! Using XFormPrimView.") # check if prim view class is found if not found_supported_prim_class: raise RuntimeError(f"Failed to find a valid prim view class for the prim paths: {self.cfg.prim_path}") @@ -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( @@ -188,19 +178,19 @@ def _initialize_warp_meshes(self): indices = np.asarray(mesh_prim.GetFaceVertexIndicesAttr().Get()) wp_mesh = convert_to_warp_mesh(points, indices, device=self.device) # print info - carb.log_info( + omni.log.info( f"Read mesh prim: {mesh_prim.GetPath()} with {len(points)} vertices and {len(indices)} faces." ) else: mesh = make_plane(size=(2e6, 2e6), height=0.0, center_zero=True) wp_mesh = convert_to_warp_mesh(mesh.vertices, mesh.faces, device=self.device) # print info - carb.log_info(f"Created infinite plane mesh prim: {mesh_prim.GetPath()}.") + 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 745033003c..27a4d323b4 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 @@ -7,15 +7,14 @@ import torch from collections.abc import Sequence -from tensordict import TensorDict from typing import TYPE_CHECKING, ClassVar, Literal +import omni.isaac.core.utils.stage as stage_utils import omni.physics.tensors.impl.api as physx from omni.isaac.core.prims import XFormPrimView import omni.isaac.lab.utils.math as math_utils from omni.isaac.lab.sensors.camera import CameraData -from omni.isaac.lab.sensors.camera.utils import convert_orientation_convention, create_rotation_matrix_from_view from omni.isaac.lab.utils.warp import raycast_mesh from .ray_caster import RayCaster @@ -87,7 +86,7 @@ def __str__(self) -> str: f"Ray-Caster-Camera @ '{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}\n" @@ -170,7 +169,7 @@ def set_world_poses( - :obj:`"ros"` - forward axis: +Z - up axis -Y - Offset is applied in the ROS convention - :obj:`"world"` - forward axis: +X - up axis +Z - Offset is applied in the World Frame convention - See :meth:`omni.isaac.lab.sensors.camera.utils.convert_orientation_convention` for more details + See :meth:`omni.isaac.lab.utils.maths.convert_camera_frame_orientation_convention` for more details on the conventions. Args: @@ -196,7 +195,9 @@ def set_world_poses( self._offset_pos[env_ids] = math_utils.quat_apply(math_utils.quat_inv(quat_w), pos_offset_world_frame) if orientations is not None: # convert rotation matrix from input convention to world - quat_w_set = convert_orientation_convention(orientations, origin=convention, target="world") + quat_w_set = math_utils.convert_camera_frame_orientation_convention( + orientations, origin=convention, target="world" + ) self._offset_quat[env_ids] = math_utils.quat_mul(math_utils.quat_inv(quat_w), quat_w_set) # update the data @@ -218,8 +219,12 @@ def set_world_poses_from_view( RuntimeError: If the camera prim is not set. Need to call :meth:`initialize` method first. NotImplementedError: If the stage up-axis is not "Y" or "Z". """ + # get up axis of current stage + up_axis = stage_utils.get_stage_up_axis() # camera position and rotation in opengl convention - orientations = math_utils.quat_from_matrix(create_rotation_matrix_from_view(eyes, targets, device=self._device)) + orientations = math_utils.quat_from_matrix( + math_utils.create_rotation_matrix_from_view(eyes, targets, up_axis=up_axis, device=self._device) + ) self.set_world_poses(eyes, orientations, env_ids, convention="opengl") """ @@ -243,7 +248,7 @@ def _initialize_rays_impl(self): # create buffer to store ray hits self.ray_hits_w = torch.zeros(self._view.count, self.num_rays, 3, device=self._device) # set offsets - quat_w = convert_orientation_convention( + quat_w = math_utils.convert_camera_frame_orientation_convention( torch.tensor([self.cfg.offset.rot], device=self._device), origin=self.cfg.offset.convention, target="world" ) self._offset_quat = quat_w.repeat(self._view.count, 1) @@ -275,7 +280,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"]] @@ -292,14 +297,23 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): ) )[:, :, 0] # apply the maximum distance after the transformation - distance_to_image_plane = torch.clip(distance_to_image_plane, max=self.cfg.max_distance) + if self.cfg.depth_clipping_behavior == "max": + distance_to_image_plane = torch.clip(distance_to_image_plane, max=self.cfg.max_distance) + distance_to_image_plane[torch.isnan(distance_to_image_plane)] = self.cfg.max_distance + elif self.cfg.depth_clipping_behavior == "zero": + distance_to_image_plane[distance_to_image_plane > self.cfg.max_distance] = 0.0 + distance_to_image_plane[torch.isnan(distance_to_image_plane)] = 0.0 self._data.output["distance_to_image_plane"][env_ids] = distance_to_image_plane.view( -1, *self.image_shape, 1 ) + if "distance_to_camera" in self.cfg.data_types: - self._data.output["distance_to_camera"][env_ids] = torch.clip( - ray_depth.view(-1, *self.image_shape, 1), max=self.cfg.max_distance - ) + if self.cfg.depth_clipping_behavior == "max": + ray_depth = torch.clip(ray_depth, max=self.cfg.max_distance) + elif self.cfg.depth_clipping_behavior == "zero": + ray_depth[ray_depth > self.cfg.max_distance] = 0.0 + self._data.output["distance_to_camera"][env_ids] = ray_depth.view(-1, *self.image_shape, 1) + if "normals" in self.cfg.data_types: self._data.output["normals"][env_ids] = ray_normal.view(-1, *self.image_shape, 3) @@ -341,7 +355,7 @@ def _create_buffers(self): self._data.image_shape = self.image_shape # -- output data # create the buffers to store the annotator data. - self._data.output = TensorDict({}, batch_size=self._view.count, device=self.device) + self._data.output = {} self._data.info = [{name: None for name in self.cfg.data_types}] * self._view.count for name in self.cfg.data_types: if name in ["distance_to_image_plane", "distance_to_camera"]: diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/ray_caster/ray_caster_camera_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/ray_caster/ray_caster_camera_cfg.py index d770031794..9654f4f3b7 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/ray_caster/ray_caster_camera_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/ray_caster/ray_caster_camera_cfg.py @@ -46,6 +46,15 @@ class OffsetCfg: data_types: list[str] = ["distance_to_image_plane"] """List of sensor names/types to enable for the camera. Defaults to ["distance_to_image_plane"].""" + depth_clipping_behavior: Literal["max", "zero", "none"] = "zero" + """Clipping behavior for the camera for values exceed the maximum value. Defaults to "zero". + + - ``"max"``: Values are clipped to the maximum value. + - ``"zero"``: Values are clipped to zero. + - ``"none``: No clipping is applied. Values will be returned as ``inf`` for ``distance_to_camera`` and ``nan`` + for ``distance_to_image_plane`` data type. + """ + pattern_cfg: PinholeCameraPatternCfg = MISSING """The pattern that defines the local ray starting positions and directions in a pinhole camera pattern.""" 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/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/__init__.py index b722e083ea..2228a2322d 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/__init__.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/__init__.py @@ -28,7 +28,7 @@ from .converters import * # noqa: F401, F403 from .schemas import * # noqa: F401, F403 -from .simulation_cfg import PhysxCfg, SimulationCfg # noqa: F401, F403 +from .simulation_cfg import PhysxCfg, RenderCfg, SimulationCfg # noqa: F401, F403 from .simulation_context import SimulationContext, build_simulation_context # noqa: F401, F403 from .spawners import * # noqa: F401, F403 from .utils import * # noqa: F401, F403 diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/asset_converter_base.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/asset_converter_base.py index 34523f5396..8da8ca2f80 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/asset_converter_base.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/asset_converter_base.py @@ -54,6 +54,8 @@ def __init__(self, cfg: AssetConverterBaseCfg): Raises: ValueError: When provided asset file does not exist. """ + # check that the config is valid + cfg.validate() # check if the asset file exists if not check_file_path(cfg.asset_path): raise ValueError(f"The asset path does not exist: {cfg.asset_path}") diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/mesh_converter.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/mesh_converter.py index 8a42138644..ecaaa4098b 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/mesh_converter.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/mesh_converter.py @@ -10,7 +10,7 @@ import omni.kit.commands import omni.usd from omni.isaac.core.utils.extensions import enable_extension -from pxr import Usd, UsdGeom, UsdPhysics, UsdUtils +from pxr import Gf, Tf, Usd, UsdGeom, UsdPhysics, UsdUtils from omni.isaac.lab.sim.converters.asset_converter_base import AssetConverterBase from omni.isaac.lab.sim.converters.mesh_converter_cfg import MeshConverterCfg @@ -64,12 +64,13 @@ def __init__(self, cfg: MeshConverterCfg): def _convert_asset(self, cfg: MeshConverterCfg): """Generate USD from OBJ, STL or FBX. - It stores the asset in the following format: + The USD file has Y-up axis and is scaled to meters. + The asset hierarchy is arranged as follows: - /file_name (default prim) - |- /geometry <- Made instanceable if requested - |- /Looks - |- /mesh + .. code-block:: none + mesh_file_basename (default prim) + |- /geometry/Looks + |- /geometry/mesh Args: cfg: The configuration for conversion of mesh to USD. @@ -81,17 +82,37 @@ def _convert_asset(self, cfg: MeshConverterCfg): mesh_file_basename, mesh_file_format = os.path.basename(cfg.asset_path).split(".") mesh_file_format = mesh_file_format.lower() + # Check if mesh_file_basename is a valid USD identifier + if not Tf.IsValidIdentifier(mesh_file_basename): + # Correct the name to a valid identifier and update the basename + mesh_file_basename_original = mesh_file_basename + mesh_file_basename = Tf.MakeValidIdentifier(mesh_file_basename) + omni.log.warn( + f"Input file name '{mesh_file_basename_original}' is an invalid identifier for the mesh prim path." + f" Renaming it to '{mesh_file_basename}' for the conversion." + ) + # Convert USD asyncio.get_event_loop().run_until_complete( - self._convert_mesh_to_usd( - in_file=cfg.asset_path, out_file=self.usd_path, prim_path=f"/{mesh_file_basename}" - ) + self._convert_mesh_to_usd(in_file=cfg.asset_path, out_file=self.usd_path) ) + # Create a new stage, set Z up and meters per unit + temp_stage = Usd.Stage.CreateInMemory() + UsdGeom.SetStageUpAxis(temp_stage, UsdGeom.Tokens.z) + UsdGeom.SetStageMetersPerUnit(temp_stage, 1.0) + UsdPhysics.SetStageKilogramsPerUnit(temp_stage, 1.0) + # Add mesh to stage + base_prim = temp_stage.DefinePrim(f"/{mesh_file_basename}", "Xform") + prim = temp_stage.DefinePrim(f"/{mesh_file_basename}/geometry", "Xform") + prim.GetReferences().AddReference(self.usd_path) + temp_stage.SetDefaultPrim(base_prim) + temp_stage.Export(self.usd_path) + # Open converted USD stage - # note: This opens a new stage and does not use the stage created earlier by the user - # create a new stage stage = Usd.Stage.Open(self.usd_path) - # add USD to stage cache + # Need to reload the stage to get the new prim structure, otherwise it can be taken from the cache + stage.Reload() + # Add USD to stage cache stage_id = UsdUtils.StageCache.Get().Insert(stage) # Get the default prim (which is the root prim) -- "/{mesh_file_basename}" xform_prim = stage.GetDefaultPrim() @@ -111,6 +132,32 @@ def _convert_asset(self, cfg: MeshConverterCfg): ) # Delete the old Xform and make the new Xform the default prim stage.SetDefaultPrim(xform_prim) + # Apply default Xform rotation to mesh -> enable to set rotation and scale + omni.kit.commands.execute( + "CreateDefaultXformOnPrimCommand", + prim_path=xform_prim.GetPath(), + **{"stage": stage}, + ) + + # Apply translation, rotation, and scale to the Xform + geom_xform = UsdGeom.Xform(geom_prim) + geom_xform.ClearXformOpOrder() + + # Remove any existing rotation attributes + rotate_attr = geom_prim.GetAttribute("xformOp:rotateXYZ") + if rotate_attr: + geom_prim.RemoveProperty(rotate_attr.GetName()) + + # translation + translate_op = geom_xform.AddTranslateOp(UsdGeom.XformOp.PrecisionDouble) + translate_op.Set(Gf.Vec3d(*cfg.translation)) + # rotation + orient_op = geom_xform.AddOrientOp(UsdGeom.XformOp.PrecisionDouble) + orient_op.Set(Gf.Quatd(*cfg.rotation)) + # scale + scale_op = geom_xform.AddScaleOp(UsdGeom.XformOp.PrecisionDouble) + scale_op.Set(Gf.Vec3d(*cfg.scale)) + # Handle instanceable # Create a new Xform prim that will be the prototype prim if cfg.make_instanceable: @@ -148,28 +195,18 @@ def _convert_asset(self, cfg: MeshConverterCfg): """ @staticmethod - async def _convert_mesh_to_usd( - in_file: str, out_file: str, prim_path: str = "/World", load_materials: bool = True - ) -> bool: + async def _convert_mesh_to_usd(in_file: str, out_file: str, load_materials: bool = True) -> bool: """Convert mesh from supported file types to USD. This function uses the Omniverse Asset Converter extension to convert a mesh file to USD. It is an asynchronous function and should be called using `asyncio.get_event_loop().run_until_complete()`. The converted asset is stored in the USD format in the specified output file. - The USD file has Y-up axis and is scaled to meters. - - The asset hierarchy is arranged as follows: - - .. code-block:: none - prim_path (default prim) - |- /geometry/Looks - |- /geometry/mesh + The USD file has Y-up axis and is scaled to cm. Args: in_file: The file to convert. out_file: The path to store the output file. - prim_path: The prim path of the mesh. load_materials: Set to True to enable attaching materials defined in the input file to the generated USD mesh. Defaults to True. @@ -177,11 +214,9 @@ async def _convert_mesh_to_usd( True if the conversion succeeds. """ enable_extension("omni.kit.asset_converter") - enable_extension("omni.usd.metrics.assembler") import omni.kit.asset_converter import omni.usd - from omni.metrics.assembler.core import get_metrics_assembler_interface # Create converter context converter_context = omni.kit.asset_converter.AssetConverterContext() @@ -202,29 +237,9 @@ async def _convert_mesh_to_usd( # Create converter task instance = omni.kit.asset_converter.get_instance() - out_file_non_metric = out_file.replace(".usd", "_non_metric.usd") - task = instance.create_converter_task(in_file, out_file_non_metric, None, converter_context) + task = instance.create_converter_task(in_file, out_file, None, converter_context) # Start conversion task and wait for it to finish - success = True - while True: - success = await task.wait_until_finished() - if not success: - await asyncio.sleep(0.1) - else: - break - - temp_stage = Usd.Stage.CreateInMemory() - UsdGeom.SetStageUpAxis(temp_stage, UsdGeom.Tokens.z) - UsdGeom.SetStageMetersPerUnit(temp_stage, 1.0) - UsdPhysics.SetStageKilogramsPerUnit(temp_stage, 1.0) - - base_prim = temp_stage.DefinePrim(prim_path, "Xform") - prim = temp_stage.DefinePrim(f"{prim_path}/geometry", "Xform") - prim.GetReferences().AddReference(out_file_non_metric) - cache = UsdUtils.StageCache.Get() - cache.Insert(temp_stage) - stage_id = cache.GetId(temp_stage).ToLongInt() - get_metrics_assembler_interface().resolve_stage(stage_id) - temp_stage.SetDefaultPrim(base_prim) - temp_stage.Export(out_file) + success = await task.wait_until_finished() + if not success: + raise RuntimeError(f"Failed to convert {in_file} to USD. Error: {task.get_error_message()}") return success diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/mesh_converter_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/mesh_converter_cfg.py index 2466048447..92d5a611e2 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/mesh_converter_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/mesh_converter_cfg.py @@ -12,21 +12,21 @@ class MeshConverterCfg(AssetConverterBaseCfg): """The configuration class for MeshConverter.""" - mass_props: schemas_cfg.MassPropertiesCfg = None + mass_props: schemas_cfg.MassPropertiesCfg | None = None """Mass properties to apply to the USD. Defaults to None. Note: If None, then no mass properties will be added. """ - rigid_props: schemas_cfg.RigidBodyPropertiesCfg = None + rigid_props: schemas_cfg.RigidBodyPropertiesCfg | None = None """Rigid body properties to apply to the USD. Defaults to None. Note: If None, then no rigid body properties will be added. """ - collision_props: schemas_cfg.CollisionPropertiesCfg = None + collision_props: schemas_cfg.CollisionPropertiesCfg | None = None """Collision properties to apply to the USD. Defaults to None. Note: @@ -42,3 +42,12 @@ class MeshConverterCfg(AssetConverterBaseCfg): "none" causes no collision mesh to be added. """ + + translation: tuple[float, float, float] = (0.0, 0.0, 0.0) + """The translation of the mesh to the origin. Defaults to (0.0, 0.0, 0.0).""" + + rotation: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) + """The rotation of the mesh in quaternion format (w, x, y, z). Defaults to (1.0, 0.0, 0.0, 0.0).""" + + scale: tuple[float, float, float] = (1.0, 1.0, 1.0) + """The scale of the mesh. Defaults to (1.0, 1.0, 1.0).""" diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/urdf_converter_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/urdf_converter_cfg.py index 06e13b3d01..1c7ebb855a 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/urdf_converter_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/urdf_converter_cfg.py @@ -30,7 +30,7 @@ class UrdfConverterCfg(AssetConverterBaseCfg): """Decompose a convex mesh into smaller pieces for a closer fit. Defaults to False.""" fix_base: bool = MISSING - """Create a fix joint to the root/base link. Defaults to True.""" + """Create a fix joint to the root/base link.""" merge_fixed_joints: bool = False """Consolidate links that are connected by fixed joints. Defaults to False.""" diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/schemas/schemas.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/schemas/schemas.py index 31a608d579..a39ba10a57 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/schemas/schemas.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/schemas/schemas.py @@ -6,8 +6,8 @@ # needed to import for allowing type-hinting: Usd.Stage | None from __future__ import annotations -import carb import omni.isaac.core.utils.stage as stage_utils +import omni.log import omni.physx.scripts.utils as physx_utils from omni.physx.scripts import deformableUtils as deformable_utils from pxr import PhysxSchema, Usd, UsdPhysics @@ -131,12 +131,12 @@ def modify_articulation_root_properties( # if we found a fixed joint, enable/disable it based on the input # otherwise, create a fixed joint between the world and the root link if existing_fixed_joint_prim is not None: - carb.log_info( + omni.log.info( f"Found an existing fixed joint for the articulation: '{prim_path}'. Setting it to: {fix_root_link}." ) existing_fixed_joint_prim.GetJointEnabledAttr().Set(fix_root_link) elif fix_root_link: - carb.log_info(f"Creating a fixed joint for the articulation: '{prim_path}'.") + omni.log.info(f"Creating a fixed joint for the articulation: '{prim_path}'.") # note: we have to assume that the root prim is a rigid body, # i.e. we don't handle the case where the root prim is not a rigid body but has articulation api on it @@ -500,10 +500,10 @@ def activate_contact_sensors(prim_path: str, threshold: float = 0.0, stage: Usd. rb.CreateSleepThresholdAttr().Set(0.0) # add contact report API with threshold of zero if not child_prim.HasAPI(PhysxSchema.PhysxContactReportAPI): - carb.log_verbose(f"Adding contact report API to prim: '{child_prim.GetPrimPath()}'") + omni.log.verbose(f"Adding contact report API to prim: '{child_prim.GetPrimPath()}'") cr_api = PhysxSchema.PhysxContactReportAPI.Apply(child_prim) else: - carb.log_verbose(f"Contact report API already exists on prim: '{child_prim.GetPrimPath()}'") + omni.log.verbose(f"Contact report API already exists on prim: '{child_prim.GetPrimPath()}'") cr_api = PhysxSchema.PhysxContactReportAPI.Get(stage, child_prim.GetPrimPath()) # set threshold to zero cr_api.CreateThresholdAttr().Set(threshold) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/simulation_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/simulation_cfg.py index 0705f5264d..a75d1f028b 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/simulation_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/simulation_cfg.py @@ -152,6 +152,54 @@ class PhysxCfg: """Size of particle contacts stream buffer allocated in pinned host memory. Default is 2 ** 20.""" +@configclass +class RenderCfg: + """Configuration for Omniverse RTX Renderer. + + These parameters are used to configure the Omniverse RTX Renderer. + For more information, see the `Omniverse RTX Renderer documentation`_. + + .. _Omniverse RTX Renderer documentation: https://docs.omniverse.nvidia.com/materials-and-rendering/latest/rtx-renderer.html + """ + + enable_translucency: bool = False + """Enables translucency for specular transmissive surfaces such as glass at the cost of some performance. Default is False.""" + + enable_reflections: bool = False + """Enables reflections at the cost of some performance. Default is False.""" + + enable_global_illumination: bool = False + """Enables Diffused Global Illumination at the cost of some performance. Default is False.""" + + antialiasing_mode: Literal["Off", "FXAA", "DLSS", "TAA", "DLAA"] = "DLSS" + """Selects the anti-aliasing mode to use. Defaults to DLSS.""" + + enable_dlssg: bool = False + """"Enables the use of DLSS-G. + DLSS Frame Generation boosts performance by using AI to generate more frames. + DLSS analyzes sequential frames and motion data to create additional high quality frames. + This feature requires an Ada Lovelace architecture GPU. + Enabling this feature also enables additional thread-related activities, which can hurt performance. + Default is False.""" + + dlss_mode: Literal[0, 1, 2, 3] = 0 + """For DLSS anti-aliasing, selects the performance/quality tradeoff mode. + Valid values are 0 (Performance), 1 (Balanced), 2 (Quality), or 3 (Auto). Default is 0.""" + + enable_direct_lighting: bool = True + """Enable direct light contributions from lights.""" + + samples_per_pixel: int = 1 + """Defines the Direct Lighting samples per pixel. + Higher values increase the direct lighting quality at the cost of performance. Default is 1.""" + + enable_shadows: bool = True + """Enables shadows at the cost of performance. When disabled, lights will not cast shadows. Defaults to True.""" + + enable_ambient_occlusion: bool = False + """Enables ambient occlusion at the cost of some performance. Default is False.""" + + @configclass class SimulationCfg: """Configuration for simulation physics.""" @@ -234,3 +282,6 @@ class SimulationCfg: The material is created at the path: ``{physics_prim_path}/defaultMaterial``. """ + + render: RenderCfg = RenderCfg() + """Render settings. Default is RenderCfg().""" 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 a8b4cd4fae..104afc538d 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 @@ -16,6 +16,7 @@ import carb import omni.isaac.core.utils.stage as stage_utils +import omni.log import omni.physx from omni.isaac.core.simulation_context import SimulationContext as _SimulationContext from omni.isaac.core.utils.viewports import set_camera_view @@ -113,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: @@ -151,6 +154,26 @@ def __init__(self, cfg: SimulationCfg | None = None): # flag for whether any GUI will be rendered (local, livestreamed or viewport) self._has_gui = self._local_gui or self._livestream_gui + # apply render settings from render config + carb_settings_iface.set_bool("/rtx/translucency/enabled", self.cfg.render.enable_translucency) + carb_settings_iface.set_bool("/rtx/reflections/enabled", self.cfg.render.enable_reflections) + carb_settings_iface.set_bool("/rtx/indirectDiffuse/enabled", self.cfg.render.enable_global_illumination) + carb_settings_iface.set_bool("/rtx/transient/dlssg/enabled", self.cfg.render.enable_dlssg) + carb_settings_iface.set_int("/rtx/post/dlss/execMode", self.cfg.render.dlss_mode) + carb_settings_iface.set_bool("/rtx/directLighting/enabled", self.cfg.render.enable_direct_lighting) + carb_settings_iface.set_int( + "/rtx/directLighting/sampledLighting/samplesPerPixel", self.cfg.render.samples_per_pixel + ) + carb_settings_iface.set_bool("/rtx/shadows/enabled", self.cfg.render.enable_shadows) + carb_settings_iface.set_bool("/rtx/ambientOcclusion/enabled", self.cfg.render.enable_ambient_occlusion) + # set denoiser mode + try: + import omni.replicator.core as rep + + rep.settings.set_render_rtx_realtime(antialiasing=self.cfg.render.antialiasing_mode) + except Exception: + pass + # store the default render mode if not self._has_gui and not self._offscreen_render: # set default render mode @@ -297,8 +320,8 @@ def get_version(self) -> tuple[int, int, int]: Operations - New utilities. """ - @staticmethod def set_camera_view( + self, eye: tuple[float, float, float], target: tuple[float, float, float], camera_prim_path: str = "/OmniverseKit_Persp", @@ -315,7 +338,9 @@ def set_camera_view( camera_prim_path: The path to the camera primitive in the stage. Defaults to "/OmniverseKit_Persp". """ - set_camera_view(eye, target, camera_prim_path) + # safe call only if we have a GUI or viewport rendering enabled + if self._has_gui or self._offscreen_render or self._render_viewport: + set_camera_view(eye, target, camera_prim_path) def set_render_mode(self, mode: RenderMode): """Change the current render mode of the simulation. @@ -336,7 +361,7 @@ def set_render_mode(self, mode: RenderMode): """ # check if mode change is possible -- not possible when no GUI is available if not self._has_gui: - carb.log_warn( + omni.log.warn( f"Cannot change render mode when GUI is disabled. Using the default render mode: {self.render_mode}." ) return @@ -614,7 +639,7 @@ def _app_control_on_stop_callback(self, event: carb.events.IEvent): if event.type == int(omni.timeline.TimelineEventType.STOP): # keep running the simulator when configured to not shutdown the app if self._has_gui and sys.exc_info()[0] is None: - self.app.print_and_log( + omni.log.warn( "Simulation is stopped. The app will keep running with physics disabled." " Press Ctrl+C or close the window to exit the app." ) @@ -649,7 +674,7 @@ def _app_control_on_stop_callback(self, event: carb.events.IEvent): omni.usd.get_context().close_stage() # print logging information - self.app.print_and_log("Simulation is stopped. Shutting down the app...") + print("[INFO]: Simulation is stopped. Shutting down the app.") # Cleanup any running tracy instances so data is not lost try: @@ -758,7 +783,7 @@ def build_simulation_context( yield sim except Exception: - carb.log_error(traceback.format_exc()) + omni.log.error(traceback.format_exc()) raise finally: if not sim.has_gui(): 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/from_files/from_files.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/from_files/from_files.py index cab6bf06c9..cbd9932b97 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/from_files/from_files.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/from_files/from_files.py @@ -7,10 +7,10 @@ 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 import omni.kit.commands +import omni.log from pxr import Gf, Sdf, Usd from omni.isaac.lab.sim import converters, schemas @@ -226,7 +226,7 @@ def _spawn_from_usd_file( scale=cfg.scale, ) else: - carb.log_warn(f"A prim already exists at prim path: '{prim_path}'.") + omni.log.warn(f"A prim already exists at prim path: '{prim_path}'.") # modify variants if hasattr(cfg, "variants") and cfg.variants is not None: diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/sensors/sensors.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/sensors/sensors.py index aa649b7e89..b05f804ce8 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/sensors/sensors.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/sensors/sensors.py @@ -7,9 +7,9 @@ from typing import TYPE_CHECKING -import carb import omni.isaac.core.utils.prims as prim_utils import omni.kit.commands +import omni.log from pxr import Sdf, Usd from omni.isaac.lab.sim.utils import clone @@ -104,7 +104,7 @@ def spawn_camera( # TODO: Adjust to handle aperture offsets once supported by omniverse # Internal ticket from rendering team: OM-42611 if cfg.horizontal_aperture_offset > 1e-4 or cfg.vertical_aperture_offset > 1e-4: - carb.log_warn("Camera aperture offsets are not supported by Omniverse. These parameters will be ignored.") + omni.log.warn("Camera aperture offsets are not supported by Omniverse. These parameters will be ignored.") # custom attributes in the config that are not USD Camera parameters non_usd_cfg_param_names = [ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/sensors/sensors_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/sensors/sensors_cfg.py index 52cd602b4c..ac36ca9485 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/sensors/sensors_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/sensors/sensors_cfg.py @@ -184,17 +184,21 @@ class FisheyeCameraCfg(PinholeCameraCfg): func: Callable = sensors.spawn_camera projection_type: Literal[ - "fisheye_orthographic", "fisheye_equidistant", "fisheye_equisolid", "fisheye_polynomial", "fisheye_spherical" - ] = "fisheye_polynomial" - r"""Type of projection to use for the camera. Defaults to "fisheye_polynomial". + "fisheyePolynomial", + "fisheyeSpherical", + "fisheyeKannalaBrandtK3", + "fisheyeRadTanThinPrism", + "omniDirectionalStereo", + ] = "fisheyePolynomial" + r"""Type of projection to use for the camera. Defaults to "fisheyePolynomial". Available options: - - ``"fisheye_orthographic"``: Fisheye camera model using orthographic correction. - - ``"fisheye_equidistant"``: Fisheye camera model using equidistant correction. - - ``"fisheye_equisolid"``: Fisheye camera model using equisolid correction. - - ``"fisheye_polynomial"``: Fisheye camera model with :math:`360^{\circ}` spherical projection. - - ``"fisheye_spherical"``: Fisheye camera model with :math:`360^{\circ}` full-frame projection. + - ``"fisheyePolynomial"``: Fisheye camera model with :math:`360^{\circ}` spherical projection. + - ``"fisheyeSpherical"``: Fisheye camera model with :math:`360^{\circ}` full-frame projection. + - ``"fisheyeKannalaBrandtK3"``: Fisheye camera model using the Kannala-Brandt K3 distortion model. + - ``"fisheyeRadTanThinPrism"``: Fisheye camera model that combines radial and tangential distortions. + - ``"omniDirectionalStereo"``: Fisheye camera model supporting :math:`360^{\circ}` stereoscopic imaging. """ fisheye_nominal_width: float = 1936.0 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/omni/isaac/lab/sim/utils.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/utils.py index 0828e4887a..526e4244e1 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/utils.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/utils.py @@ -13,9 +13,9 @@ from collections.abc import Callable from typing import TYPE_CHECKING, Any -import carb import omni.isaac.core.utils.stage as stage_utils import omni.kit.commands +import omni.log from omni.isaac.cloner import Cloner from pxr import PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics, UsdShade @@ -70,7 +70,7 @@ def safe_set_attribute_on_usd_schema(schema_api: Usd.APISchemaBase, name: str, v else: # think: do we ever need to create the attribute if it doesn't exist? # currently, we are not doing this since the schemas are already created with some defaults. - carb.log_error(f"Attribute '{attr_name}' does not exist on prim '{schema_api.GetPath()}'.") + omni.log.error(f"Attribute '{attr_name}' does not exist on prim '{schema_api.GetPath()}'.") raise TypeError(f"Attribute '{attr_name}' does not exist on prim '{schema_api.GetPath()}'.") @@ -189,7 +189,7 @@ def wrapper(prim_path: str | Sdf.Path, *args, **kwargs): count_success += 1 # check if we were successful in applying the function to any prim if count_success == 0: - carb.log_warn( + omni.log.warn( f"Could not perform '{func.__name__}' on any prims under: '{prim_path}'." " This might be because of the following reasons:" "\n\t(1) The desired attribute does not exist on any of the prims." @@ -391,7 +391,7 @@ def bind_physics_material( has_deformable_body = prim.HasAPI(PhysxSchema.PhysxDeformableBodyAPI) has_particle_system = prim.IsA(PhysxSchema.PhysxParticleSystem) if not (has_physics_scene_api or has_collider or has_deformable_body or has_particle_system): - carb.log_verbose( + omni.log.verbose( f"Cannot apply physics material '{material_path}' on prim '{prim_path}'. It is neither a" " PhysX scene, collider, a deformable body, nor a particle system." ) @@ -851,14 +851,14 @@ class TableVariants: for variant_set_name, variant_selection in variants.items(): # Check if the variant set exists on the prim. if not existing_variant_sets.HasVariantSet(variant_set_name): - carb.log_warn(f"Variant set '{variant_set_name}' does not exist on prim '{prim_path}'.") + omni.log.warn(f"Variant set '{variant_set_name}' does not exist on prim '{prim_path}'.") continue variant_set = existing_variant_sets.GetVariantSet(variant_set_name) # Only set the variant selection if it is different from the current selection. if variant_set.GetVariantSelection() != variant_selection: variant_set.SetVariantSelection(variant_selection) - carb.log_info( + omni.log.info( f"Setting variant selection '{variant_selection}' for variant set '{variant_set_name}' on" f" prim '{prim_path}'." ) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_generator.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_generator.py index abd411a856..e91b51f117 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_generator.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/terrains/terrain_generator.py @@ -8,7 +8,7 @@ import torch import trimesh -import carb +import omni.log from omni.isaac.lab.utils.dict import dict_to_md5_hash from omni.isaac.lab.utils.io import dump_yaml @@ -119,7 +119,7 @@ def __init__(self, cfg: TerrainGeneratorCfg, device: str = "cpu"): # throw a warning if the cache is enabled but the seed is not set if self.cfg.use_cache and self.cfg.seed is None: - carb.log_warn( + omni.log.warn( "Cache is enabled but the seed is not set. The terrain generation will not be reproducible." " Please set the seed in the terrain generator configuration to make the generation reproducible." ) @@ -295,7 +295,7 @@ def _add_sub_terrain( """ # sample flat patches if specified if sub_terrain_cfg.flat_patch_sampling is not None: - carb.log_info(f"Sampling flat patches for sub-terrain at (row, col): ({row}, {col})") + omni.log.info(f"Sampling flat patches for sub-terrain at (row, col): ({row}, {col})") # convert the mesh to warp mesh wp_mesh = convert_to_warp_mesh(mesh.vertices, mesh.faces, device=self.device) # sample flat patches based on each patch configuration for that sub-terrain 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/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/__init__.py index 23fb50d4c2..1cfa3507fd 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/__init__.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/__init__.py @@ -13,3 +13,4 @@ from .modifiers import * from .string import * from .timer import Timer +from .types import * 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/omni/isaac/lab/utils/datasets/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/datasets/__init__.py new file mode 100644 index 0000000000..40ed823c7d --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/datasets/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Submodule for datasets classes and methods. +""" + +from .dataset_file_handler_base import DatasetFileHandlerBase +from .episode_data import EpisodeData +from .hdf5_dataset_file_handler import HDF5DatasetFileHandler diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/datasets/dataset_file_handler_base.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/datasets/dataset_file_handler_base.py new file mode 100644 index 0000000000..4dabba566c --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/datasets/dataset_file_handler_base.py @@ -0,0 +1,58 @@ +# Copyright (c) 2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from abc import ABC, abstractmethod + +from .episode_data import EpisodeData + + +class DatasetFileHandlerBase(ABC): + """Abstract class for handling dataset files.""" + + def __init__(self): + """Initializes the dataset file handler.""" + pass + + @abstractmethod + def open(self, file_path: str, mode: str = "r"): + """Open a file.""" + return NotImplementedError + + @abstractmethod + def create(self, file_path: str, env_name: str = None): + """Create a new file.""" + return NotImplementedError + + @abstractmethod + def get_env_name(self) -> str | None: + """Get the environment name.""" + return NotImplementedError + + @abstractmethod + def write_episode(self, episode: EpisodeData): + """Write episode data to the file.""" + return NotImplementedError + + @abstractmethod + def flush(self): + """Flush the file.""" + return NotImplementedError + + @abstractmethod + def close(self): + """Close the file.""" + return NotImplementedError + + @abstractmethod + def load_episode(self, episode_name: str) -> EpisodeData | None: + """Load episode data from the file.""" + return NotImplementedError + + @abstractmethod + def get_num_episodes(self) -> int: + """Get number of episodes in the file.""" + return NotImplementedError diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/datasets/episode_data.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/datasets/episode_data.py new file mode 100644 index 0000000000..57a1c306cb --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/datasets/episode_data.py @@ -0,0 +1,171 @@ +# Copyright (c) 2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch + + +class EpisodeData: + """Class to store episode data.""" + + def __init__(self) -> None: + """Initializes episode data class.""" + self._data = dict() + self._next_action_index = 0 + self._next_state_index = 0 + self._seed = None + self._env_id = None + self._success = None + + @property + def data(self): + """Returns the episode data.""" + return self._data + + @data.setter + def data(self, data: dict): + """Set the episode data.""" + self._data = data + + @property + def seed(self): + """Returns the random number generator seed.""" + return self._seed + + @seed.setter + def seed(self, seed: int): + """Set the random number generator seed.""" + self._seed = seed + + @property + def env_id(self): + """Returns the environment ID.""" + return self._env_id + + @env_id.setter + def env_id(self, env_id: int): + """Set the environment ID.""" + self._env_id = env_id + + @property + def next_action_index(self): + """Returns the next action index.""" + return self._next_action_index + + @next_action_index.setter + def next_action_index(self, index: int): + """Set the next action index.""" + self._next_action_index = index + + @property + def next_state_index(self): + """Returns the next state index.""" + return self._next_state_index + + @next_state_index.setter + def next_state_index(self, index: int): + """Set the next state index.""" + self._next_state_index = index + + @property + def success(self): + """Returns the success value.""" + return self._success + + @success.setter + def success(self, success: bool): + """Set the success value.""" + self._success = success + + def is_empty(self): + """Check if the episode data is empty.""" + return not bool(self._data) + + def add(self, key: str, value: torch.Tensor | dict): + """Add a key-value pair to the dataset. + + The key can be nested by using the "/" character. + For example: "obs/joint_pos". + + Args: + key: The key name. + value: The corresponding value of tensor type or of dict type. + """ + # check datatype + if isinstance(value, dict): + for sub_key, sub_value in value.items(): + self.add(f"{key}/{sub_key}", sub_value) + return + + sub_keys = key.split("/") + current_dataset_pointer = self._data + for sub_key_index in range(len(sub_keys)): + if sub_key_index == len(sub_keys) - 1: + # Add value to the final dict layer + if sub_keys[sub_key_index] not in current_dataset_pointer: + current_dataset_pointer[sub_keys[sub_key_index]] = value.unsqueeze(0).clone() + else: + current_dataset_pointer[sub_keys[sub_key_index]] = torch.cat( + (current_dataset_pointer[sub_keys[sub_key_index]], value.unsqueeze(0)) + ) + break + # key index + if sub_keys[sub_key_index] not in current_dataset_pointer: + current_dataset_pointer[sub_keys[sub_key_index]] = dict() + current_dataset_pointer = current_dataset_pointer[sub_keys[sub_key_index]] + + def get_initial_state(self) -> torch.Tensor | None: + """Get the initial state from the dataset.""" + if "initial_state" not in self._data: + return None + return self._data["initial_state"] + + def get_action(self, action_index) -> torch.Tensor | None: + """Get the action of the specified index from the dataset.""" + if "actions" not in self._data: + return None + if action_index >= len(self._data["actions"]): + return None + return self._data["actions"][action_index] + + def get_next_action(self) -> torch.Tensor | None: + """Get the next action from the dataset.""" + action = self.get_action(self._next_action_index) + if action is not None: + self._next_action_index += 1 + return action + + def get_state(self, state_index) -> dict | None: + """Get the state of the specified index from the dataset.""" + if "states" not in self._data: + return None + + states = self._data["states"] + + def get_state_helper(states, state_index) -> dict | torch.Tensor | None: + if isinstance(states, dict): + output_state = dict() + for key, value in states.items(): + output_state[key] = get_state_helper(value, state_index) + if output_state[key] is None: + return None + elif isinstance(states, torch.Tensor): + if state_index >= len(states): + return None + output_state = states[state_index] + else: + raise ValueError(f"Invalid state type: {type(states)}") + return output_state + + output_state = get_state_helper(states, state_index) + return output_state + + def get_next_state(self) -> dict | None: + """Get the next state from the dataset.""" + state = self.get_state(self._next_state_index) + if state is not None: + self._next_state_index += 1 + return state diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/datasets/hdf5_dataset_file_handler.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/datasets/hdf5_dataset_file_handler.py new file mode 100644 index 0000000000..f399030b5e --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/datasets/hdf5_dataset_file_handler.py @@ -0,0 +1,192 @@ +# Copyright (c) 2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import h5py +import json +import numpy as np +import os +import torch +from collections.abc import Iterable + +from .dataset_file_handler_base import DatasetFileHandlerBase +from .episode_data import EpisodeData + + +class HDF5DatasetFileHandler(DatasetFileHandlerBase): + """HDF5 dataset file handler for storing and loading episode data.""" + + def __init__(self): + """Initializes the HDF5 dataset file handler.""" + self._hdf5_file_stream = None + self._hdf5_data_group = None + self._demo_count = 0 + self._env_args = {} + + def open(self, file_path: str, mode: str = "r"): + """Open an existing dataset file.""" + if self._hdf5_file_stream is not None: + raise RuntimeError("HDF5 dataset file stream is already in use") + self._hdf5_file_stream = h5py.File(file_path, mode) + self._hdf5_data_group = self._hdf5_file_stream["data"] + self._demo_count = len(self._hdf5_data_group) + + def create(self, file_path: str, env_name: str = None): + """Create a new dataset file.""" + if self._hdf5_file_stream is not None: + raise RuntimeError("HDF5 dataset file stream is already in use") + if not file_path.endswith(".hdf5"): + file_path += ".hdf5" + dir_path = os.path.dirname(file_path) + if not os.path.isdir(dir_path): + os.makedirs(dir_path) + self._hdf5_file_stream = h5py.File(file_path, "w") + + # set up a data group in the file + self._hdf5_data_group = self._hdf5_file_stream.create_group("data") + self._hdf5_data_group.attrs["total"] = 0 + self._demo_count = 0 + + # set environment arguments + # the environment type (we use gym environment type) is set to be compatible with robomimic + # Ref: https://github.com/ARISE-Initiative/robomimic/blob/master/robomimic/envs/env_base.py#L15 + env_name = env_name if env_name is not None else "" + self.add_env_args({"env_name": env_name, "type": 2}) + + def __del__(self): + """Destructor for the file handler.""" + self.close() + + """ + Properties + """ + + def add_env_args(self, env_args: dict): + """Add environment arguments to the dataset.""" + self._raise_if_not_initialized() + self._env_args.update(env_args) + self._hdf5_data_group.attrs["env_args"] = json.dumps(self._env_args) + + def set_env_name(self, env_name: str): + """Set the environment name.""" + self._raise_if_not_initialized() + self.add_env_args({"env_name": env_name}) + + def get_env_name(self) -> str | None: + """Get the environment name.""" + self._raise_if_not_initialized() + env_args = json.loads(self._hdf5_data_group.attrs["env_args"]) + if "env_name" in env_args: + return env_args["env_name"] + return None + + def get_episode_names(self) -> Iterable[str]: + """Get the names of the episodes in the file.""" + self._raise_if_not_initialized() + return self._hdf5_data_group.keys() + + def get_num_episodes(self) -> int: + """Get number of episodes in the file.""" + return self._demo_count + + @property + def demo_count(self) -> int: + """The number of demos collected so far.""" + return self._demo_count + + """ + Operations. + """ + + def load_episode(self, episode_name: str, device: str) -> EpisodeData | None: + """Load episode data from the file.""" + self._raise_if_not_initialized() + if episode_name not in self._hdf5_data_group: + return None + episode = EpisodeData() + h5_episode_group = self._hdf5_data_group[episode_name] + + def load_dataset_helper(group): + """Helper method to load dataset that contains recursive dict objects.""" + data = {} + for key in group: + if isinstance(group[key], h5py.Group): + data[key] = load_dataset_helper(group[key]) + else: + # Converting group[key] to numpy array greatly improves the performance + # when converting to torch tensor + data[key] = torch.tensor(np.array(group[key]), device=device) + return data + + episode.data = load_dataset_helper(h5_episode_group) + + if "seed" in h5_episode_group.attrs: + episode.seed = h5_episode_group.attrs["seed"] + + if "success" in h5_episode_group.attrs: + episode.success = h5_episode_group.attrs["success"] + + episode.env_id = self.get_env_name() + + return episode + + def write_episode(self, episode: EpisodeData): + """Add an episode to the dataset. + + Args: + episode: The episode data to add. + """ + self._raise_if_not_initialized() + if episode.is_empty(): + return + + # create episode group based on demo count + h5_episode_group = self._hdf5_data_group.create_group(f"demo_{self._demo_count}") + + # store number of steps taken + if "actions" in episode.data: + h5_episode_group.attrs["num_samples"] = len(episode.data["actions"]) + else: + h5_episode_group.attrs["num_samples"] = 0 + + if episode.seed is not None: + h5_episode_group.attrs["seed"] = episode.seed + + if episode.success is not None: + h5_episode_group.attrs["success"] = episode.success + + def create_dataset_helper(group, key, value): + """Helper method to create dataset that contains recursive dict objects.""" + if isinstance(value, dict): + key_group = group.create_group(key) + for sub_key, sub_value in value.items(): + create_dataset_helper(key_group, sub_key, sub_value) + else: + group.create_dataset(key, data=value.cpu().numpy()) + + for key, value in episode.data.items(): + create_dataset_helper(h5_episode_group, key, value) + + # increment total step counts + self._hdf5_data_group.attrs["total"] += h5_episode_group.attrs["num_samples"] + + # increment total demo counts + self._demo_count += 1 + + def flush(self): + """Flush the episode data to disk.""" + self._raise_if_not_initialized() + + self._hdf5_file_stream.flush() + + def close(self): + """Close the dataset file handler.""" + if self._hdf5_file_stream is not None: + self._hdf5_file_stream.close() + self._hdf5_file_stream = None + + def _raise_if_not_initialized(self): + """Raise an error if the dataset file handler is not initialized.""" + if self._hdf5_file_stream is None: + raise RuntimeError("HDF5 dataset file stream is not initialized") diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/dict.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/dict.py index ca6b4f2a4b..07086a1f9b 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/dict.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/dict.py @@ -40,8 +40,10 @@ def class_to_dict(obj: object) -> dict[str, Any]: # convert object to dictionary if isinstance(obj, dict): obj_dict = obj - else: + elif hasattr(obj, "__dict__"): obj_dict = obj.__dict__ + else: + return obj # convert to dictionary data = dict() @@ -55,6 +57,8 @@ def class_to_dict(obj: object) -> dict[str, Any]: # check if attribute is a dictionary elif hasattr(value, "__dict__") or isinstance(value, dict): data[key] = class_to_dict(value) + elif isinstance(value, (list, tuple)): + data[key] = type(value)([class_to_dict(v) for v in value]) else: data[key] = value return data diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/math.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/math.py index 1d3c0db81f..42c6a2bc0d 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/math.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/math.py @@ -8,6 +8,7 @@ # needed to import for allowing type-hinting: torch.Tensor | np.ndarray from __future__ import annotations +import math import numpy as np import torch import torch.nn.functional @@ -1418,3 +1419,143 @@ def sample_cylinder( xyz[..., 2].uniform_(h_min, h_max) # return positions return xyz + + +""" +Orientation Conversions +""" + + +def convert_camera_frame_orientation_convention( + orientation: torch.Tensor, + origin: Literal["opengl", "ros", "world"] = "opengl", + target: Literal["opengl", "ros", "world"] = "ros", +) -> torch.Tensor: + r"""Converts a quaternion representing a rotation from one convention to another. + + In USD, the camera follows the ``"opengl"`` convention. Thus, it is always in **Y up** convention. + This means that the camera is looking down the -Z axis with the +Y axis pointing up , and +X axis pointing right. + However, in ROS, the camera is looking down the +Z axis with the +Y axis pointing down, and +X axis pointing right. + Thus, the camera needs to be rotated by :math:`180^{\circ}` around the X axis to follow the ROS convention. + + .. math:: + + T_{ROS} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & -1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T_{USD} + + On the other hand, the typical world coordinate system is with +X pointing forward, +Y pointing left, + and +Z pointing up. The camera can also be set in this convention by rotating the camera by :math:`90^{\circ}` + around the X axis and :math:`-90^{\circ}` around the Y axis. + + .. math:: + + T_{WORLD} = \begin{bmatrix} 0 & 0 & -1 & 0 \\ -1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T_{USD} + + Thus, based on their application, cameras follow different conventions for their orientation. This function + converts a quaternion from one convention to another. + + Possible conventions are: + + - :obj:`"opengl"` - forward axis: -Z - up axis +Y - Offset is applied in the OpenGL (Usd.Camera) convention + - :obj:`"ros"` - forward axis: +Z - up axis -Y - Offset is applied in the ROS convention + - :obj:`"world"` - forward axis: +X - up axis +Z - Offset is applied in the World Frame convention + + Args: + orientation: Quaternion of form `(w, x, y, z)` with shape (..., 4) in source convention. + origin: Convention to convert from. Defaults to "opengl". + target: Convention to convert to. Defaults to "ros". + + Returns: + Quaternion of form `(w, x, y, z)` with shape (..., 4) in target convention + """ + if target == origin: + return orientation.clone() + + # -- unify input type + if origin == "ros": + # convert from ros to opengl convention + rotm = matrix_from_quat(orientation) + rotm[:, :, 2] = -rotm[:, :, 2] + rotm[:, :, 1] = -rotm[:, :, 1] + # convert to opengl convention + quat_gl = quat_from_matrix(rotm) + elif origin == "world": + # convert from world (x forward and z up) to opengl convention + rotm = matrix_from_quat(orientation) + rotm = torch.matmul( + rotm, + matrix_from_euler(torch.tensor([math.pi / 2, -math.pi / 2, 0], device=orientation.device), "XYZ"), + ) + # convert to isaac-sim convention + quat_gl = quat_from_matrix(rotm) + else: + quat_gl = orientation + + # -- convert to target convention + if target == "ros": + # convert from opengl to ros convention + rotm = matrix_from_quat(quat_gl) + rotm[:, :, 2] = -rotm[:, :, 2] + rotm[:, :, 1] = -rotm[:, :, 1] + return quat_from_matrix(rotm) + elif target == "world": + # convert from opengl to world (x forward and z up) convention + rotm = matrix_from_quat(quat_gl) + rotm = torch.matmul( + rotm, + matrix_from_euler(torch.tensor([math.pi / 2, -math.pi / 2, 0], device=orientation.device), "XYZ").T, + ) + return quat_from_matrix(rotm) + else: + return quat_gl.clone() + + +def create_rotation_matrix_from_view( + eyes: torch.Tensor, + targets: torch.Tensor, + up_axis: Literal["Y", "Z"] = "Z", + device: str = "cpu", +) -> torch.Tensor: + """Compute the rotation matrix from world to view coordinates. + + This function takes a vector ''eyes'' which specifies the location + of the camera in world coordinates and the vector ''targets'' which + indicate the position of the object. + The output is a rotation matrix representing the transformation + from world coordinates -> view coordinates. + + The inputs eyes and targets can each be a + - 3 element tuple/list + - torch tensor of shape (1, 3) + - torch tensor of shape (N, 3) + + Args: + eyes: Position of the camera in world coordinates. + targets: Position of the object in world coordinates. + up_axis: The up axis of the camera. Defaults to "Z". + device: The device to create torch tensors on. Defaults to "cpu". + + The vectors are broadcast against each other so they all have shape (N, 3). + + Returns: + R: (N, 3, 3) batched rotation matrices + + Reference: + Based on PyTorch3D (https://github.com/facebookresearch/pytorch3d/blob/eaf0709d6af0025fe94d1ee7cec454bc3054826a/pytorch3d/renderer/cameras.py#L1635-L1685) + """ + if up_axis == "Y": + up_axis_vec = torch.tensor((0, 1, 0), device=device, dtype=torch.float32).repeat(eyes.shape[0], 1) + elif up_axis == "Z": + up_axis_vec = torch.tensor((0, 0, 1), device=device, dtype=torch.float32).repeat(eyes.shape[0], 1) + else: + raise ValueError(f"Invalid up axis: {up_axis}. Valid options are 'Y' and 'Z'.") + + # get rotation matrix in opengl format (-Z forward, +Y up) + z_axis = -torch.nn.functional.normalize(targets - eyes, eps=1e-5) + x_axis = torch.nn.functional.normalize(torch.cross(up_axis_vec, z_axis, dim=1), eps=1e-5) + y_axis = torch.nn.functional.normalize(torch.cross(z_axis, x_axis, dim=1), eps=1e-5) + is_close = torch.isclose(x_axis, torch.tensor(0.0), atol=5e-3).all(dim=1, keepdim=True) + if is_close.any(): + replacement = torch.nn.functional.normalize(torch.cross(y_axis, z_axis, dim=1), eps=1e-5) + x_axis = torch.where(is_close, replacement, x_axis) + R = torch.cat((x_axis[:, None, :], y_axis[:, None, :], z_axis[:, None, :]), dim=1) + return R.transpose(1, 2) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/noise/noise_model.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/noise/noise_model.py index 4bd2ff09f4..4f3e2e09c1 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/noise/noise_model.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/noise/noise_model.py @@ -18,7 +18,20 @@ def constant_noise(data: torch.Tensor, cfg: noise_cfg.ConstantNoiseCfg) -> torch.Tensor: - """Constant noise.""" + """Applies a constant noise bias to a given data set. + + Args: + data: The unmodified data set to apply noise to. + cfg: The configuration parameters for constant noise. + + Returns: + The data modified by the noise parameters provided. + """ + + # fix tensor device for bias on first call and update config parameters + if isinstance(cfg.bias, torch.Tensor): + cfg.bias = cfg.bias.to(device=data.device) + if cfg.operation == "add": return data + cfg.bias elif cfg.operation == "scale": @@ -30,7 +43,23 @@ def constant_noise(data: torch.Tensor, cfg: noise_cfg.ConstantNoiseCfg) -> torch def uniform_noise(data: torch.Tensor, cfg: noise_cfg.UniformNoiseCfg) -> torch.Tensor: - """Uniform noise.""" + """Applies a uniform noise to a given data set. + + Args: + data: The unmodified data set to apply noise to. + cfg: The configuration parameters for uniform noise. + + Returns: + The data modified by the noise parameters provided. + """ + + # fix tensor device for n_max on first call and update config parameters + if isinstance(cfg.n_max, torch.Tensor): + cfg.n_max = cfg.n_max.to(data.device) + # fix tensor device for n_min on first call and update config parameters + if isinstance(cfg.n_min, torch.Tensor): + cfg.n_min = cfg.n_min.to(data.device) + if cfg.operation == "add": return data + torch.rand_like(data) * (cfg.n_max - cfg.n_min) + cfg.n_min elif cfg.operation == "scale": @@ -42,7 +71,23 @@ def uniform_noise(data: torch.Tensor, cfg: noise_cfg.UniformNoiseCfg) -> torch.T def gaussian_noise(data: torch.Tensor, cfg: noise_cfg.GaussianNoiseCfg) -> torch.Tensor: - """Gaussian noise.""" + """Applies a gaussian noise to a given data set. + + Args: + data: The unmodified data set to apply noise to. + cfg: The configuration parameters for gaussian noise. + + Returns: + The data modified by the noise parameters provided. + """ + + # fix tensor device for mean on first call and update config parameters + if isinstance(cfg.mean, torch.Tensor): + cfg.mean = cfg.mean.to(data.device) + # fix tensor device for std on first call and update config parameters + if isinstance(cfg.std, torch.Tensor): + cfg.std = cfg.std.to(data.device) + if cfg.operation == "add": return data + cfg.mean + cfg.std * torch.randn_like(data) elif cfg.operation == "scale": diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/types.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/types.py new file mode 100644 index 0000000000..3f2066a192 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/types.py @@ -0,0 +1,39 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Sub-module for different data types.""" + +from __future__ import annotations + +import torch +from collections.abc import Sequence +from dataclasses import dataclass + + +@dataclass +class ArticulationActions: + """Data container to store articulation's joints actions. + + This class is used to store the actions of the joints of an articulation. + It is used to store the joint positions, velocities, efforts, and indices. + + If the actions are not provided, the values are set to None. + """ + + joint_positions: torch.Tensor | None = None + """The joint positions of the articulation. Defaults to None.""" + + joint_velocities: torch.Tensor | None = None + """The joint velocities of the articulation. Defaults to None.""" + + joint_efforts: torch.Tensor | None = None + """The joint efforts of the articulation. Defaults to None.""" + + joint_indices: torch.Tensor | Sequence[int] | slice | None = None + """The joint indices of the articulation. Defaults to None. + + If the joint indices are a slice, this indicates that the indices are continuous and correspond + to all the joints of the articulation. We use a slice to make the indexing more efficient. + """ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/warp/kernels.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/warp/kernels.py index e401fc6e72..f7efbc78a6 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/warp/kernels.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/warp/kernels.py @@ -10,7 +10,7 @@ import warp as wp -@wp.kernel +@wp.kernel(enable_backward=False) def raycast_mesh_kernel( mesh: wp.uint64, ray_starts: wp.array(dtype=wp.vec3), @@ -75,7 +75,7 @@ def raycast_mesh_kernel( ray_face_id[tid] = f -@wp.kernel +@wp.kernel(enable_backward=False) def reshape_tiled_image( tiled_image_buffer: Any, batched_image: Any, diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/warp/ops.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/warp/ops.py index 5c128fa1ea..23658152e0 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/warp/ops.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/utils/warp/ops.py @@ -13,6 +13,9 @@ import warp as wp +# disable warp module initialization messages +wp.config.quiet = True +# initialize the warp module wp.init() from . import kernels diff --git a/source/extensions/omni.isaac.lab/setup.py b/source/extensions/omni.isaac.lab/setup.py index 3b68d3c84f..06c92832e5 100644 --- a/source/extensions/omni.isaac.lab/setup.py +++ b/source/extensions/omni.isaac.lab/setup.py @@ -22,15 +22,18 @@ "torch==2.4.0", "onnx==1.16.1", # 1.16.2 throws access violation on Windows "prettytable==3.3.0", - "tensordict", "toml", # devices "hidapi", # reinforcement learning - "gymnasium==0.29.0", + "gymnasium", # procedural-generation "trimesh", "pyglet<2", + # image processing + "transformers", + "einops", # needed for transformers, doesn't always auto-install + "warp-lang", ] PYTORCH_INDEX_URL = ["https://download.pytorch.org/whl/cu118"] diff --git a/source/extensions/omni.isaac.lab/test/assets/test_articulation.py b/source/extensions/omni.isaac.lab/test/assets/test_articulation.py index 2fc0910ee9..d96283be6a 100644 --- a/source/extensions/omni.isaac.lab/test/assets/test_articulation.py +++ b/source/extensions/omni.isaac.lab/test/assets/test_articulation.py @@ -42,6 +42,8 @@ def generate_articulation_cfg( articulation_type: Literal["humanoid", "panda", "anymal", "shadow_hand", "single_joint"], stiffness: float | None = 10.0, damping: float | None = 2.0, + vel_limit: float | None = 100.0, + effort_limit: float | None = 400.0, ) -> ArticulationCfg: """Generate an articulation configuration. @@ -72,8 +74,8 @@ def generate_articulation_cfg( actuators={ "joint": ImplicitActuatorCfg( joint_names_expr=[".*"], - effort_limit=400.0, - velocity_limit=100.0, + effort_limit=effort_limit, + velocity_limit=vel_limit, stiffness=0.0, damping=10.0, ), @@ -542,6 +544,72 @@ def test_out_of_range_default_joint_vel(self): # Check if articulation is initialized self.assertFalse(articulation._is_initialized) + def test_joint_limits(self): + """Test write_joint_limits_to_sim API and when default pos falls outside of the new limits.""" + for num_articulations in (1, 2): + for device in ("cuda:0", "cpu"): + with self.subTest(num_articulations=num_articulations, device=device): + with build_simulation_context(device=device, add_ground_plane=True, auto_add_lighting=True) as sim: + # Create articulation + articulation_cfg = generate_articulation_cfg(articulation_type="panda") + articulation, _ = generate_articulation(articulation_cfg, num_articulations, device) + + # Play sim + sim.reset() + # Check if articulation is initialized + self.assertTrue(articulation._is_initialized) + + # Get current default joint pos + default_joint_pos = articulation._data.default_joint_pos.clone() + + # Set new joint limits + limits = torch.zeros(num_articulations, articulation.num_joints, 2, device=device) + limits[..., 0] = ( + torch.rand(num_articulations, articulation.num_joints, device=device) + 5.0 + ) * -1.0 + limits[..., 1] = torch.rand(num_articulations, articulation.num_joints, device=device) + 5.0 + articulation.write_joint_limits_to_sim(limits) + + # Check new limits are in place + torch.testing.assert_close(articulation._data.joint_limits, limits) + torch.testing.assert_close(articulation._data.default_joint_pos, default_joint_pos) + + # Set new joint limits with indexing + env_ids = torch.arange(1, device=device) + joint_ids = torch.arange(2, device=device) + limits = torch.zeros(env_ids.shape[0], joint_ids.shape[0], 2, device=device) + limits[..., 0] = (torch.rand(env_ids.shape[0], joint_ids.shape[0], device=device) + 5.0) * -1.0 + limits[..., 1] = torch.rand(env_ids.shape[0], joint_ids.shape[0], device=device) + 5.0 + articulation.write_joint_limits_to_sim(limits, env_ids=env_ids, joint_ids=joint_ids) + + # Check new limits are in place + torch.testing.assert_close(articulation._data.joint_limits[env_ids][:, joint_ids], limits) + torch.testing.assert_close(articulation._data.default_joint_pos, default_joint_pos) + + # Set new joint limits that invalidate default joint pos + limits = torch.zeros(num_articulations, articulation.num_joints, 2, device=device) + limits[..., 0] = torch.rand(num_articulations, articulation.num_joints, device=device) * -0.1 + limits[..., 1] = torch.rand(num_articulations, articulation.num_joints, device=device) * 0.1 + articulation.write_joint_limits_to_sim(limits) + + # Check if all values are within the bounds + within_bounds = (articulation._data.default_joint_pos >= limits[..., 0]) & ( + articulation._data.default_joint_pos <= limits[..., 1] + ) + self.assertTrue(torch.all(within_bounds)) + + # Set new joint limits that invalidate default joint pos with indexing + limits = torch.zeros(env_ids.shape[0], joint_ids.shape[0], 2, device=device) + limits[..., 0] = torch.rand(env_ids.shape[0], joint_ids.shape[0], device=device) * -0.1 + limits[..., 1] = torch.rand(env_ids.shape[0], joint_ids.shape[0], device=device) * 0.1 + articulation.write_joint_limits_to_sim(limits, env_ids=env_ids, joint_ids=joint_ids) + + # Check if all values are within the bounds + within_bounds = ( + articulation._data.default_joint_pos[env_ids][:, joint_ids] >= limits[..., 0] + ) & (articulation._data.default_joint_pos[env_ids][:, joint_ids] <= limits[..., 1]) + self.assertTrue(torch.all(within_bounds)) + def test_external_force_on_single_body(self): """Test application of external force on the base of the articulation.""" for num_articulations in (1, 2): @@ -747,6 +815,43 @@ def test_setting_gains_from_cfg_dict(self): torch.testing.assert_close(articulation.actuators["body"].stiffness, expected_stiffness) torch.testing.assert_close(articulation.actuators["body"].damping, expected_damping) + def test_setting_velocity_limits(self): + """Test that velocity limits are loaded form the configuration correctly.""" + for num_articulations in (1, 2): + for device in ("cuda:0", "cpu"): + for limit in (5.0, None): + with self.subTest(num_articulations=num_articulations, device=device, limit=limit): + with build_simulation_context( + device=device, add_ground_plane=False, auto_add_lighting=True + ) as sim: + articulation_cfg = generate_articulation_cfg( + articulation_type="single_joint", vel_limit=limit, effort_limit=limit + ) + articulation, _ = generate_articulation( + articulation_cfg=articulation_cfg, num_articulations=num_articulations, device=device + ) + # Play sim + sim.reset() + + if limit is not None: + # Expected gains + expected_velocity_limit = torch.full( + (articulation.num_instances, articulation.num_joints), + limit, + device=articulation.device, + ) + # Check that gains are loaded from USD file + torch.testing.assert_close( + articulation.actuators["joint"].velocity_limit, expected_velocity_limit + ) + torch.testing.assert_close( + articulation.data.joint_velocity_limits, expected_velocity_limit + ) + torch.testing.assert_close( + articulation.root_physx_view.get_dof_max_velocities().to(device), + expected_velocity_limit, + ) + def test_reset(self): """Test that reset method works properly. diff --git a/source/extensions/omni.isaac.lab/test/assets/test_rigid_object.py b/source/extensions/omni.isaac.lab/test/assets/test_rigid_object.py index 429264c2c3..2724f13e05 100644 --- a/source/extensions/omni.isaac.lab/test/assets/test_rigid_object.py +++ b/source/extensions/omni.isaac.lab/test/assets/test_rigid_object.py @@ -24,6 +24,7 @@ import ctypes import torch import unittest +from typing import Literal import omni.isaac.core.utils.prims as prim_utils @@ -31,19 +32,23 @@ from omni.isaac.lab.assets import RigidObject, RigidObjectCfg from omni.isaac.lab.sim import build_simulation_context from omni.isaac.lab.sim.spawners import materials -from omni.isaac.lab.utils.assets import ISAAC_NUCLEUS_DIR +from omni.isaac.lab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR from omni.isaac.lab.utils.math import default_orientation, random_orientation def generate_cubes_scene( - num_cubes: int = 1, height=1.0, has_api: bool = True, kinematic_enabled: bool = False, device: str = "cuda:0" + num_cubes: int = 1, + height=1.0, + api: Literal["none", "rigid_body", "articulation_root"] = "rigid_body", + kinematic_enabled: bool = False, + device: str = "cuda:0", ) -> tuple[RigidObject, torch.Tensor]: """Generate a scene with the provided number of cubes. Args: num_cubes: Number of cubes to generate. height: Height of the cubes. - has_api: Whether the cubes have a rigid body API on them. + api: The type of API that the cubes should have. kinematic_enabled: Whether the cubes are kinematic. device: Device to use for the simulation. @@ -57,17 +62,25 @@ def generate_cubes_scene( prim_utils.create_prim(f"/World/Table_{i}", "Xform", translation=origin) # Resolve spawn configuration - if has_api: - spawn_cfg = sim_utils.UsdFileCfg( - usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", - rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=kinematic_enabled), - ) - else: + if api == "none": # since no rigid body properties defined, this is just a static collider spawn_cfg = sim_utils.CuboidCfg( size=(0.1, 0.1, 0.1), collision_props=sim_utils.CollisionPropertiesCfg(), ) + elif api == "rigid_body": + spawn_cfg = sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=kinematic_enabled), + ) + elif api == "articulation_root": + spawn_cfg = sim_utils.UsdFileCfg( + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Tests/RigidObject/Cube/dex_cube_instanceable_with_articulation_root.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=kinematic_enabled), + ) + else: + raise ValueError(f"Unknown api: {api}") + # Create rigid object cube_object_cfg = RigidObjectCfg( prim_path="/World/Table_.*/Object", @@ -161,7 +174,27 @@ def test_initialization_with_no_rigid_body(self): with self.subTest(num_cubes=num_cubes, device=device): with build_simulation_context(device=device, auto_add_lighting=True) as sim: # Generate cubes scene - cube_object, _ = generate_cubes_scene(num_cubes=num_cubes, has_api=False, device=device) + cube_object, _ = generate_cubes_scene(num_cubes=num_cubes, api="none", device=device) + + # Check that boundedness of rigid object is correct + self.assertEqual(ctypes.c_long.from_address(id(cube_object)).value, 1) + + # Play sim + sim.reset() + + # Check if object is initialized + self.assertFalse(cube_object.is_initialized) + + def test_initialization_with_articulation_root(self): + """Test that initialization fails when an articulation root is found at the provided prim path.""" + for num_cubes in (1, 2): + for device in ("cuda:0", "cpu"): + with self.subTest(num_cubes=num_cubes, device=device): + with build_simulation_context(device=device, auto_add_lighting=True) as sim: + # Generate cubes scene + cube_object, _ = generate_cubes_scene( + num_cubes=num_cubes, api="articulation_root", device=device + ) # Check that boundedness of rigid object is correct self.assertEqual(ctypes.c_long.from_address(id(cube_object)).value, 1) @@ -426,175 +459,177 @@ def test_rigid_body_no_friction(self): cube_object.data.root_lin_vel_w, initial_velocity[:, :3], rtol=1e-5, atol=tolerance ) - # def test_rigid_body_with_static_friction(self): - # """Test that static friction applied to rigid object works as expected. - - # This test works by applying a force to the object and checking if the object moves or not based on the - # mu (coefficient of static friction) value set for the object. We set the static friction to be non-zero and - # apply a force to the object. When the force applied is below mu, the object should not move. When the force - # applied is above mu, the object should move. - # """ - # for num_cubes in (1, 2): - # for device in ("cuda:0", "cpu"): - # with self.subTest(num_cubes=num_cubes, device=device): - # with build_simulation_context(device=device, add_ground_plane=True, auto_add_lighting=True) as sim: - # cube_object, _ = generate_cubes_scene(num_cubes=num_cubes, height=0.03125, device=device) - - # # Create ground plane with no friction - # cfg = sim_utils.GroundPlaneCfg( - # physics_material=materials.RigidBodyMaterialCfg( - # static_friction=0.0, - # dynamic_friction=0.0, - # ) - # ) - # cfg.func("/World/GroundPlane", cfg) - - # # Play sim - # sim.reset() - - # # Set static friction to be non-zero - # static_friction_coefficient = 0.5 - # static_friction = torch.Tensor([[static_friction_coefficient]] * num_cubes) - # dynamic_friction = torch.zeros(num_cubes, 1) - # restitution = torch.FloatTensor(num_cubes, 1).uniform_(0.0, 0.2) - - # cube_object_materials = torch.cat([static_friction, dynamic_friction, restitution], dim=-1) - - # indices = torch.tensor(range(num_cubes), dtype=torch.int) - - # # Add friction to cube - # cube_object.root_physx_view.set_material_properties(cube_object_materials, indices) - - # # 2 cases: force applied is below and above mu - # # below mu: block should not move as the force applied is <= mu - # # above mu: block should move as the force applied is > mu - # for force in "below_mu", "above_mu": - # with self.subTest(force=force): - # external_wrench_b = torch.zeros((num_cubes, 1, 6), device=sim.device) - - # if force == "below_mu": - # external_wrench_b[:, 0, 0] = static_friction_coefficient * 0.999 - # else: - # external_wrench_b[:, 0, 0] = static_friction_coefficient * 1.001 - - # cube_object.set_external_force_and_torque( - # external_wrench_b[..., :3], - # external_wrench_b[..., 3:], - # ) - - # # Get root state - # initial_root_state = cube_object.data.root_state_w - - # # Simulate physics - # for _ in range(10): - # # perform rendering - # sim.step() - # # update object - # cube_object.update(sim.cfg.dt) - - # if force == "below_mu": - # # Assert that the block has not moved - # torch.testing.assert_close( - # cube_object.data.root_state_w, initial_root_state, rtol=1e-5, atol=1e-5 - # ) - # else: - # torch.testing.assert_close( - # cube_object.data.root_state_w, initial_root_state, rtol=1e-5, atol=1e-5 - # ) - - # def test_rigid_body_with_restitution(self): - # """Test that restitution when applied to rigid object works as expected. - - # This test works by dropping a block from a height and checking if the block bounces or not based on the - # restitution value set for the object. We set the restitution to be non-zero and drop the block from a height. - # When the restitution is 0, the block should not bounce. When the restitution is 1, the block should bounce - # with the same energy. When the restitution is between 0 and 1, the block should bounce with less energy. - - # """ - # for num_cubes in (1, 2): - # for device in ("cuda:0", "cpu"): - # with self.subTest(num_cubes=num_cubes, device=device): - # with build_simulation_context(device=device, add_ground_plane=True, auto_add_lighting=True) as sim: - # cube_object, _ = generate_cubes_scene(num_cubes=num_cubes, height=1.0, device=device) - - # # Create ground plane such that has a restitution of 1.0 (perfectly elastic collision) - # cfg = sim_utils.GroundPlaneCfg( - # physics_material=materials.RigidBodyMaterialCfg( - # restitution=1.0, - # ) - # ) - # cfg.func("/World/GroundPlane", cfg) - - # indices = torch.tensor(range(num_cubes), dtype=torch.int) - - # # Play sim - # sim.reset() - - # # 3 cases: inelastic, partially elastic, elastic - # # inelastic: resitution = 0, block should not bounce - # # partially elastic: 0 <= restitution <= 1, block should bounce with less energy - # # elastic: restitution = 1, block should bounce with same energy - # for expected_collision_type in "inelastic", "partially_elastic", "elastic": - # root_state = torch.zeros(1, 13, device=sim.device) - # root_state[0, 3] = 1.0 # To make orientation a quaternion - # root_state[0, 2] = 0.1 # Set an initial drop height - # root_state[0, 9] = -1.0 # Set an initial downward velocity - - # cube_object.write_root_state_to_sim(root_state=root_state) - - # prev_z_velocity = 0.0 - # curr_z_velocity = 0.0 - - # with self.subTest(expected_collision_type=expected_collision_type): - # # cube_object.reset() - # # Set static friction to be non-zero - # if expected_collision_type == "inelastic": - # restitution_coefficient = 0.0 - # elif expected_collision_type == "partially_elastic": - # restitution_coefficient = 0.5 - # else: - # restitution_coefficient = 1.0 - - # restitution = 0.5 - # static_friction = torch.zeros(num_cubes, 1) - # dynamic_friction = torch.zeros(num_cubes, 1) - # restitution = torch.Tensor([[restitution_coefficient]] * num_cubes) - - # cube_object_materials = torch.cat( - # [static_friction, dynamic_friction, restitution], dim=-1 - # ) - - # # Add friction to cube - # cube_object.root_physx_view.set_material_properties(cube_object_materials, indices) - - # curr_z_velocity = cube_object.data.root_lin_vel_w[:, 2] - - # while torch.all(curr_z_velocity <= 0.0): - # # Simulate physics - # curr_z_velocity = cube_object.data.root_lin_vel_w[:, 2] - - # # perform rendering - # sim.step() - - # # update object - # cube_object.update(sim.cfg.dt) - # if torch.all(curr_z_velocity <= 0.0): - # # Still in the air - # prev_z_velocity = curr_z_velocity - - # # We have made contact with the ground and can verify expected collision type - # # based on how velocity has changed after the collision - # if expected_collision_type == "inelastic": - # # Assert that the block has lost most energy by checking that the z velocity is < 1/2 previous - # # velocity. This is because the floor's resitution means it will bounce back an object that itself - # # has restitution set to 0.0 - # self.assertTrue(torch.all(torch.le(curr_z_velocity / 2, abs(prev_z_velocity)))) - # elif expected_collision_type == "partially_elastic": - # # Assert that the block has lost some energy by checking that the z velocity is less - # self.assertTrue(torch.all(torch.le(abs(curr_z_velocity), abs(prev_z_velocity)))) - # elif expected_collision_type == "elastic": - # # Assert that the block has not lost any energy by checking that the z velocity is the same - # torch.testing.assert_close(abs(curr_z_velocity), abs(prev_z_velocity)) + def test_rigid_body_with_static_friction(self): + """Test that static friction applied to rigid object works as expected. + + This test works by applying a force to the object and checking if the object moves or not based on the + mu (coefficient of static friction) value set for the object. We set the static friction to be non-zero and + apply a force to the object. When the force applied is below mu, the object should not move. When the force + applied is above mu, the object should move. + """ + for num_cubes in (1, 2): + for device in ("cuda", "cpu"): + with self.subTest(num_cubes=num_cubes, device=device): + with build_simulation_context( + device=device, dt=0.01, add_ground_plane=False, auto_add_lighting=True + ) as sim: + cube_object, _ = generate_cubes_scene(num_cubes=num_cubes, height=0.03125, device=device) + + # Create ground plane + static_friction_coefficient = 0.5 + cfg = sim_utils.GroundPlaneCfg( + physics_material=materials.RigidBodyMaterialCfg( + static_friction=static_friction_coefficient, + dynamic_friction=static_friction_coefficient, # This shouldn't be required but is due to a bug in PhysX + ) + ) + cfg.func("/World/GroundPlane", cfg) + + # Play sim + sim.reset() + + # Set static friction to be non-zero + # Dynamic friction also needs to be zero due to a bug in PhysX + static_friction = torch.Tensor([[static_friction_coefficient]] * num_cubes) + dynamic_friction = torch.Tensor([[static_friction_coefficient]] * num_cubes) + restitution = torch.zeros(num_cubes, 1) + + cube_object_materials = torch.cat([static_friction, dynamic_friction, restitution], dim=-1) + + indices = torch.tensor(range(num_cubes), dtype=torch.int) + + # Add friction to cube + cube_object.root_physx_view.set_material_properties(cube_object_materials, indices) + + # let everything settle + for _ in range(100): + sim.step() + cube_object.update(sim.cfg.dt) + cube_object.write_root_velocity_to_sim(torch.zeros((num_cubes, 6), device=sim.device)) + cube_mass = cube_object.root_physx_view.get_masses() + gravity_magnitude = abs(sim.cfg.gravity[2]) + # 2 cases: force applied is below and above mu + # below mu: block should not move as the force applied is <= mu + # above mu: block should move as the force applied is > mu + for force in "below_mu", "above_mu": + with self.subTest(force=force): + # set initial velocity to zero + cube_object.write_root_velocity_to_sim(torch.zeros((num_cubes, 6), device=sim.device)) + + external_wrench_b = torch.zeros((num_cubes, 1, 6), device=sim.device) + if force == "below_mu": + external_wrench_b[..., 0] = ( + static_friction_coefficient * cube_mass * gravity_magnitude * 0.99 + ) + else: + external_wrench_b[..., 0] = ( + static_friction_coefficient * cube_mass * gravity_magnitude * 1.01 + ) + + cube_object.set_external_force_and_torque( + external_wrench_b[..., :3], + external_wrench_b[..., 3:], + ) + + # Get root state + initial_root_pos = cube_object.data.root_pos_w.clone() + # Simulate physics + for _ in range(200): + # apply the wrench + cube_object.write_data_to_sim() + sim.step() + # update object + cube_object.update(sim.cfg.dt) + if force == "below_mu": + # Assert that the block has not moved + torch.testing.assert_close( + cube_object.data.root_pos_w, initial_root_pos, rtol=1e-3, atol=1e-3 + ) + if force == "above_mu": + self.assertTrue( + (cube_object.data.root_state_w[..., 0] - initial_root_pos[..., 0] > 0.02).all() + ) + + def test_rigid_body_with_restitution(self): + """Test that restitution when applied to rigid object works as expected. + + This test works by dropping a block from a height and checking if the block bounces or not based on the + restitution value set for the object. We set the restitution to be non-zero and drop the block from a height. + When the restitution is 0, the block should not bounce. When the restitution is between 0 and 1, the block + should bounce with less energy. + """ + for num_cubes in (1, 2): + for device in ("cuda:0", "cpu"): + for expected_collision_type in "partially_elastic", "inelastic": + with self.subTest( + expected_collision_type=expected_collision_type, num_cubes=num_cubes, device=device + ): + with build_simulation_context( + device=device, add_ground_plane=False, auto_add_lighting=True + ) as sim: + cube_object, _ = generate_cubes_scene(num_cubes=num_cubes, height=1.0, device=device) + + # Set static friction to be non-zero + if expected_collision_type == "inelastic": + restitution_coefficient = 0.0 + elif expected_collision_type == "partially_elastic": + restitution_coefficient = 0.5 + + # Create ground plane such that has a restitution of 1.0 (perfectly elastic collision) + cfg = sim_utils.GroundPlaneCfg( + physics_material=materials.RigidBodyMaterialCfg( + restitution=restitution_coefficient, + ) + ) + cfg.func("/World/GroundPlane", cfg) + + indices = torch.tensor(range(num_cubes), dtype=torch.int) + + # Play sim + sim.reset() + + root_state = torch.zeros(num_cubes, 13, device=sim.device) + root_state[:, 3] = 1.0 # To make orientation a quaternion + for i in range(num_cubes): + root_state[i, 1] = 1.0 * i + root_state[:, 2] = 1.0 # Set an initial drop height + root_state[:, 9] = -1.0 # Set an initial downward velocity + + cube_object.write_root_state_to_sim(root_state=root_state) + + static_friction = torch.zeros(num_cubes, 1) + dynamic_friction = torch.zeros(num_cubes, 1) + restitution = torch.Tensor([[restitution_coefficient]] * num_cubes) + + cube_object_materials = torch.cat([static_friction, dynamic_friction, restitution], dim=-1) + + # Add restitution to cube + cube_object.root_physx_view.set_material_properties(cube_object_materials, indices) + + curr_z_velocity = cube_object.data.root_lin_vel_w[:, 2].clone() + + for _ in range(100): + sim.step() + + # update object + cube_object.update(sim.cfg.dt) + curr_z_velocity = cube_object.data.root_lin_vel_w[:, 2].clone() + + if expected_collision_type == "inelastic": + # assert that the block has not bounced by checking that the z velocity is less than or equal to 0 + self.assertTrue((curr_z_velocity <= 0.0).all()) + + if torch.all(curr_z_velocity <= 0.0): + # Still in the air + prev_z_velocity = curr_z_velocity + else: + # collision has happened, exit the for loop + break + + if expected_collision_type == "partially_elastic": + # Assert that the block has lost some energy by checking that the z velocity is less + self.assertTrue(torch.all(torch.le(abs(curr_z_velocity), abs(prev_z_velocity)))) + self.assertTrue((curr_z_velocity > 0.0).all()) def test_rigid_body_set_mass(self): """Test getting and setting mass of rigid object.""" diff --git a/source/extensions/omni.isaac.lab/test/assets/test_rigid_object_collection.py b/source/extensions/omni.isaac.lab/test/assets/test_rigid_object_collection.py new file mode 100644 index 0000000000..5674359ab0 --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/assets/test_rigid_object_collection.py @@ -0,0 +1,506 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# ignore private usage of variables warning +# pyright: reportPrivateUsage=none + + +"""Launch Isaac Sim Simulator first.""" + +from omni.isaac.lab.app import AppLauncher, run_tests + +# Can set this to False to see the GUI for debugging +# This will also add lights to the scene +HEADLESS = True + +# launch omniverse app +app_launcher = AppLauncher(headless=HEADLESS) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import ctypes +import torch +import unittest + +import omni.isaac.core.utils.prims as prim_utils + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import RigidObjectCfg, RigidObjectCollection, RigidObjectCollectionCfg +from omni.isaac.lab.sim import build_simulation_context +from omni.isaac.lab.utils.assets import ISAAC_NUCLEUS_DIR +from omni.isaac.lab.utils.math import default_orientation, random_orientation + + +def generate_cubes_scene( + num_envs: int = 1, + num_cubes: int = 1, + height=1.0, + has_api: bool = True, + kinematic_enabled: bool = False, + device: str = "cuda:0", +) -> tuple[RigidObjectCollection, torch.Tensor]: + """Generate a scene with the provided number of cubes. + + Args: + num_envs: Number of envs to generate. + num_cubes: Number of cubes to generate. + height: Height of the cubes. + has_api: Whether the cubes have a rigid body API on them. + kinematic_enabled: Whether the cubes are kinematic. + device: Device to use for the simulation. + + Returns: + A tuple containing the rigid object representing the cubes and the origins of the cubes. + + """ + origins = torch.tensor([(i * 3.0, 0, height) for i in range(num_envs)]).to(device) + # Create Top-level Xforms, one for each cube + for i, origin in enumerate(origins): + prim_utils.create_prim(f"/World/Table_{i}", "Xform", translation=origin) + + # Resolve spawn configuration + if has_api: + spawn_cfg = sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=kinematic_enabled), + ) + else: + # since no rigid body properties defined, this is just a static collider + spawn_cfg = sim_utils.CuboidCfg( + size=(0.1, 0.1, 0.1), + collision_props=sim_utils.CollisionPropertiesCfg(), + ) + + # create the rigid object configs + cube_config_dict = {} + for i in range(num_cubes): + cube_object_cfg = RigidObjectCfg( + prim_path=f"/World/Table_.*/Object_{i}", + spawn=spawn_cfg, + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 3 * i, height)), + ) + cube_config_dict[f"cube_{i}"] = cube_object_cfg + # create the rigid object collection + cube_object_collection_cfg = RigidObjectCollectionCfg(rigid_objects=cube_config_dict) + cube_object_colection = RigidObjectCollection(cfg=cube_object_collection_cfg) + + return cube_object_colection, origins + + +class TestRigidObjectCollection(unittest.TestCase): + """Test for rigid object collection class.""" + + """ + Tests + """ + + def test_initialization(self): + """Test initialization for prim with rigid body API at the provided prim path.""" + for num_envs in (1, 2): + for num_cubes in (1, 3): + for device in ("cuda:0", "cpu"): + with self.subTest(num_envs=num_envs, num_cubes=num_cubes, device=device): + with build_simulation_context(device=device, auto_add_lighting=True) as sim: + # Generate cubes scene + object_collection, _ = generate_cubes_scene( + num_envs=num_envs, num_cubes=num_cubes, device=device + ) + + # Check that boundedness of rigid object is correct + self.assertEqual(ctypes.c_long.from_address(id(object_collection)).value, 1) + + # Play sim + sim.reset() + + # Check if object is initialized + self.assertTrue(object_collection.is_initialized) + self.assertEqual(len(object_collection.object_names), num_cubes) + + # Check buffers that exists and have correct shapes + self.assertEqual(object_collection.data.object_pos_w.shape, (num_envs, num_cubes, 3)) + self.assertEqual(object_collection.data.object_quat_w.shape, (num_envs, num_cubes, 4)) + self.assertEqual(object_collection.data.default_mass.shape, (num_envs, num_cubes, 1)) + self.assertEqual(object_collection.data.default_inertia.shape, (num_envs, num_cubes, 9)) + + # Simulate physics + for _ in range(2): + # perform rendering + sim.step() + # update object + object_collection.update(sim.cfg.dt) + + def test_id_conversion(self): + """Test environment and object index conversion to physics view indices.""" + for device in ("cuda:0", "cpu"): + with self.subTest(num_envs=2, num_cubes=3, device=device): + with build_simulation_context(device=device, auto_add_lighting=True) as sim: + # Generate cubes scene + object_collection, _ = generate_cubes_scene(num_envs=2, num_cubes=3, device=device) + + # Play sim + sim.reset() + + expected = [ + torch.tensor([4, 5], device=device, dtype=torch.long), + torch.tensor([4], device=device, dtype=torch.long), + torch.tensor([0, 2, 4], device=device, dtype=torch.long), + torch.tensor([1, 3, 5], device=device, dtype=torch.long), + ] + + view_ids = object_collection._env_obj_ids_to_view_ids( + object_collection._ALL_ENV_INDICES, object_collection._ALL_OBJ_INDICES[None, 2] + ) + self.assertTrue((view_ids == expected[0]).all()) + view_ids = object_collection._env_obj_ids_to_view_ids( + object_collection._ALL_ENV_INDICES[None, 0], object_collection._ALL_OBJ_INDICES[None, 2] + ) + self.assertTrue((view_ids == expected[1]).all()) + view_ids = object_collection._env_obj_ids_to_view_ids( + object_collection._ALL_ENV_INDICES[None, 0], object_collection._ALL_OBJ_INDICES + ) + self.assertTrue((view_ids == expected[2]).all()) + view_ids = object_collection._env_obj_ids_to_view_ids( + object_collection._ALL_ENV_INDICES[None, 1], object_collection._ALL_OBJ_INDICES + ) + self.assertTrue((view_ids == expected[3]).all()) + + def test_initialization_with_kinematic_enabled(self): + """Test that initialization for prim with kinematic flag enabled.""" + for num_envs in (1, 2): + for num_cubes in (1, 3): + for device in ("cuda:0", "cpu"): + with self.subTest(num_envs=num_envs, num_cubes=num_cubes, device=device): + with build_simulation_context(device=device, auto_add_lighting=True) as sim: + # Generate cubes scene + object_collection, origins = generate_cubes_scene( + num_envs=num_envs, num_cubes=num_cubes, kinematic_enabled=True, device=device + ) + + # Check that boundedness of rigid object is correct + self.assertEqual(ctypes.c_long.from_address(id(object_collection)).value, 1) + + # Play sim + sim.reset() + + # Check if object is initialized + self.assertTrue(object_collection.is_initialized) + self.assertEqual(len(object_collection.object_names), num_cubes) + + # Check buffers that exists and have correct shapes + self.assertEqual(object_collection.data.object_pos_w.shape, (num_envs, num_cubes, 3)) + self.assertEqual(object_collection.data.object_quat_w.shape, (num_envs, num_cubes, 4)) + + # Simulate physics + for _ in range(2): + # perform rendering + sim.step() + # update object + object_collection.update(sim.cfg.dt) + # check that the object is kinematic + default_object_state = object_collection.data.default_object_state.clone() + default_object_state[..., :3] += origins.unsqueeze(1) + torch.testing.assert_close(object_collection.data.object_state_w, default_object_state) + + def test_initialization_with_no_rigid_body(self): + """Test that initialization fails when no rigid body is found at the provided prim path.""" + for num_cubes in (1, 2): + for device in ("cuda:0", "cpu"): + with self.subTest(num_cubes=num_cubes, device=device): + with build_simulation_context(device=device, auto_add_lighting=True) as sim: + # Generate cubes scene + object_collection, _ = generate_cubes_scene(num_cubes=num_cubes, has_api=False, device=device) + + # Check that boundedness of rigid object is correct + self.assertEqual(ctypes.c_long.from_address(id(object_collection)).value, 1) + + # Play sim + sim.reset() + + # Check if object is initialized + self.assertFalse(object_collection.is_initialized) + + def test_external_force_on_single_body(self): + """Test application of external force on the base of the object. + + In this test, we apply a force equal to the weight of an object on the base of + one of the objects. We check that the object does not move. For the other object, + we do not apply any force and check that it falls down. + """ + for num_envs in (1, 2): + for num_cubes in (1, 4): + for device in ("cuda:0", "cpu"): + with self.subTest(num_envs=num_envs, num_cubes=num_cubes, device=device): + # Generate cubes scene + with build_simulation_context( + device=device, add_ground_plane=True, auto_add_lighting=True + ) as sim: + object_collection, origins = generate_cubes_scene( + num_envs=num_envs, num_cubes=num_cubes, device=device + ) + + # Play the simulator + sim.reset() + + # Find objects to apply the force + object_ids, object_names = object_collection.find_objects(".*") + + # Sample a force equal to the weight of the object + external_wrench_b = torch.zeros( + object_collection.num_instances, len(object_ids), 6, device=sim.device + ) + # Every 2nd cube should have a force applied to it + external_wrench_b[:, 0::2, 2] = 9.81 * object_collection.data.default_mass[:, 0::2, 0] + + # Now we are ready! + for _ in range(5): + # reset object state + object_state = object_collection.data.default_object_state.clone() + + # need to shift the position of the cubes otherwise they will be on top of each other + object_state[..., :2] += origins.unsqueeze(1)[..., :2] + object_collection.write_object_state_to_sim(object_state) + + # reset object + object_collection.reset() + + # apply force + object_collection.set_external_force_and_torque( + external_wrench_b[..., :3], external_wrench_b[..., 3:], object_ids=object_ids + ) + # perform simulation + for _ in range(10): + # apply action to the object + object_collection.write_data_to_sim() + + # perform step + sim.step() + + # update buffers + object_collection.update(sim.cfg.dt) + + # First object should still be at the same Z position (1.0) + torch.testing.assert_close( + object_collection.data.object_pos_w[:, 0::2, 2], + torch.ones_like(object_collection.data.object_pos_w[:, 0::2, 2]), + ) + # Second object should have fallen, so it's Z height should be less than initial height of 1.0 + self.assertTrue(torch.all(object_collection.data.object_pos_w[:, 1::2, 2] < 1.0)) + + def test_set_object_state(self): + """Test setting the state of the object. + + In this test, we set the state of the object to a random state and check + that the object is in that state after simulation. We set gravity to zero as + we don't want any external forces acting on the object to ensure state remains static. + """ + for num_envs in (1, 3): + for num_cubes in (1, 2): + for device in ("cuda:0", "cpu"): + with self.subTest(num_envs=num_envs, num_cubes=num_cubes, device=device): + # Turn off gravity for this test as we don't want any external forces acting on the object + # to ensure state remains static + with build_simulation_context( + device=device, gravity_enabled=False, auto_add_lighting=True + ) as sim: + # Generate cubes scene + object_collection, origins = generate_cubes_scene( + num_envs=num_envs, num_cubes=num_cubes, device=device + ) + + # Play the simulator + sim.reset() + + state_types = ["object_pos_w", "object_quat_w", "object_lin_vel_w", "object_ang_vel_w"] + + # Set each state type individually as they are dependent on each other + for state_type_to_randomize in state_types: + state_dict = { + "object_pos_w": torch.zeros_like( + object_collection.data.object_pos_w, device=sim.device + ), + "object_quat_w": default_orientation( + num=num_cubes * num_envs, device=sim.device + ).view(num_envs, num_cubes, 4), + "object_lin_vel_w": torch.zeros_like( + object_collection.data.object_lin_vel_w, device=sim.device + ), + "object_ang_vel_w": torch.zeros_like( + object_collection.data.object_ang_vel_w, device=sim.device + ), + } + + # Now we are ready! + for _ in range(5): + # reset object + object_collection.reset() + + # Set random state + if state_type_to_randomize == "object_quat_w": + state_dict[state_type_to_randomize] = random_orientation( + num=num_cubes * num_envs, device=sim.device + ).view(num_envs, num_cubes, 4) + else: + state_dict[state_type_to_randomize] = torch.randn( + num_envs, num_cubes, 3, device=sim.device + ) + # make sure objects do not overlap + if state_type_to_randomize == "object_pos_w": + state_dict[state_type_to_randomize][..., :2] += origins.unsqueeze(1)[ + ..., :2 + ] + + # perform simulation + for _ in range(5): + object_state = torch.cat( + [ + state_dict["object_pos_w"], + state_dict["object_quat_w"], + state_dict["object_lin_vel_w"], + state_dict["object_ang_vel_w"], + ], + dim=-1, + ) + # reset object state + object_collection.write_object_state_to_sim(object_state=object_state) + + sim.step() + + # assert that set object quantities are equal to the ones set in the state_dict + for key, expected_value in state_dict.items(): + value = getattr(object_collection.data, key) + torch.testing.assert_close(value, expected_value, rtol=1e-5, atol=1e-5) + + object_collection.update(sim.cfg.dt) + + def test_reset_object_collection(self): + """Test resetting the state of the rigid object.""" + for num_envs in (1, 3): + for num_cubes in (1, 2): + for device in ("cuda:0", "cpu"): + with self.subTest(num_envs=num_envs, num_cubes=num_cubes, device=device): + with build_simulation_context( + device=device, gravity_enabled=True, auto_add_lighting=True + ) as sim: + # Generate cubes scene + object_collection, _ = generate_cubes_scene( + num_envs=num_envs, num_cubes=num_cubes, device=device + ) + + # Play the simulator + sim.reset() + + for i in range(5): + # perform rendering + sim.step() + + # update object + object_collection.update(sim.cfg.dt) + + # Move the object to a random position + object_state = object_collection.data.default_object_state.clone() + object_state[..., :3] = torch.randn(num_envs, num_cubes, 3, device=sim.device) + + # Random orientation + object_state[..., 3:7] = random_orientation(num=num_cubes, device=sim.device) + object_collection.write_object_state_to_sim(object_state) + + if i % 2 == 0: + # reset object + object_collection.reset() + + # Reset should zero external forces and torques + self.assertFalse(object_collection.has_external_wrench) + self.assertEqual(torch.count_nonzero(object_collection._external_force_b), 0) + self.assertEqual(torch.count_nonzero(object_collection._external_torque_b), 0) + + def test_set_material_properties(self): + """Test getting and setting material properties of rigid object.""" + for num_envs in (1, 3): + for num_cubes in (1, 2): + for device in ("cuda:0", "cpu"): + with self.subTest(num_envs=num_envs, num_cubes=num_cubes, device=device): + with build_simulation_context( + device=device, gravity_enabled=True, add_ground_plane=True, auto_add_lighting=True + ) as sim: + # Generate cubes scene + object_collection, _ = generate_cubes_scene( + num_envs=num_envs, num_cubes=num_cubes, device=device + ) + + # Play sim + sim.reset() + + # Set material properties + static_friction = torch.FloatTensor(num_envs, num_cubes, 1).uniform_(0.4, 0.8) + dynamic_friction = torch.FloatTensor(num_envs, num_cubes, 1).uniform_(0.4, 0.8) + restitution = torch.FloatTensor(num_envs, num_cubes, 1).uniform_(0.0, 0.2) + + materials = torch.cat([static_friction, dynamic_friction, restitution], dim=-1) + + indices = torch.tensor(range(num_cubes * num_envs), dtype=torch.int) + # Add friction to cube + object_collection.root_physx_view.set_material_properties( + object_collection.reshape_data_to_view(materials), indices + ) + + # Simulate physics + sim.step() + # update object + object_collection.update(sim.cfg.dt) + + # Get material properties + materials_to_check = object_collection.root_physx_view.get_material_properties() + + # Check if material properties are set correctly + torch.testing.assert_close( + object_collection.reshape_view_to_data(materials_to_check), materials + ) + + def test_gravity_vec_w(self): + """Test that gravity vector direction is set correctly for the rigid object.""" + for num_envs in (1, 3): + for num_cubes in (1, 2): + for device in ("cuda:0", "cpu"): + for gravity_enabled in [True, False]: + with self.subTest( + num_envs=num_envs, num_cubes=num_cubes, device=device, gravity_enabled=gravity_enabled + ): + with build_simulation_context(device=device, gravity_enabled=gravity_enabled) as sim: + # Create a scene with random cubes + object_collection, _ = generate_cubes_scene( + num_envs=num_envs, num_cubes=num_cubes, device=device + ) + + # Obtain gravity direction + if gravity_enabled: + gravity_dir = (0.0, 0.0, -1.0) + else: + gravity_dir = (0.0, 0.0, 0.0) + + # Play sim + sim.reset() + + # Check that gravity is set correctly + self.assertEqual(object_collection.data.GRAVITY_VEC_W[0, 0, 0], gravity_dir[0]) + self.assertEqual(object_collection.data.GRAVITY_VEC_W[0, 0, 1], gravity_dir[1]) + self.assertEqual(object_collection.data.GRAVITY_VEC_W[0, 0, 2], gravity_dir[2]) + + # Simulate physics + for _ in range(2): + sim.step() + # update object + object_collection.update(sim.cfg.dt) + + # Expected gravity value is the acceleration of the body + gravity = torch.zeros(num_envs, num_cubes, 6, device=device) + if gravity_enabled: + gravity[..., 2] = -9.81 + # Check the body accelerations are correct + torch.testing.assert_close(object_collection.data.object_acc_w, gravity) + + +if __name__ == "__main__": + run_tests() diff --git a/source/extensions/omni.isaac.lab/test/controllers/test_differential_ik.py b/source/extensions/omni.isaac.lab/test/controllers/test_differential_ik.py index 4d5877aaa1..17a069d5b9 100644 --- a/source/extensions/omni.isaac.lab/test/controllers/test_differential_ik.py +++ b/source/extensions/omni.isaac.lab/test/controllers/test_differential_ik.py @@ -22,7 +22,14 @@ import omni.isaac.lab.sim as sim_utils from omni.isaac.lab.assets import Articulation from omni.isaac.lab.controllers import DifferentialIKController, DifferentialIKControllerCfg -from omni.isaac.lab.utils.math import compute_pose_error, subtract_frame_transforms + +from omni.isaac.lab.utils.math import ( # isort:skip + compute_pose_error, + matrix_from_quat, + quat_inv, + random_yaw_orientation, + subtract_frame_transforms, +) ## # Pre-defined configs @@ -169,6 +176,10 @@ def _run_ik_controller( robot.write_joint_state_to_sim(joint_pos, joint_vel) robot.set_joint_position_target(joint_pos) robot.write_data_to_sim() + # randomize root state yaw, ik should work regardless base rotation + root_state = robot.data.root_state_w.clone() + root_state[:, 3:7] = random_yaw_orientation(self.num_envs, self.sim.device) + robot.write_root_state_to_sim(root_state) robot.reset() # reset actions ee_pose_b_des[:] = self.ee_pose_b_des_set[current_goal_idx] @@ -185,6 +196,10 @@ def _run_ik_controller( jacobian = robot.root_physx_view.get_jacobians()[:, ee_jacobi_idx, :, arm_joint_ids] ee_pose_w = robot.data.body_state_w[:, ee_frame_idx, 0:7] root_pose_w = robot.data.root_state_w[:, 0:7] + base_rot = root_pose_w[:, 3:7] + base_rot_matrix = matrix_from_quat(quat_inv(base_rot)) + jacobian[:, :3, :] = torch.bmm(base_rot_matrix, jacobian[:, :3, :]) + jacobian[:, 3:, :] = torch.bmm(base_rot_matrix, jacobian[:, 3:, :]) joint_pos = robot.data.joint_pos[:, arm_joint_ids] # compute frame in root frame ee_pos_b, ee_quat_b = subtract_frame_transforms( diff --git a/source/extensions/omni.isaac.lab/test/deps/isaacsim/check_floating_base_made_fixed.py b/source/extensions/omni.isaac.lab/test/deps/isaacsim/check_floating_base_made_fixed.py index d77f037eb3..b2a4471133 100644 --- a/source/extensions/omni.isaac.lab/test/deps/isaacsim/check_floating_base_made_fixed.py +++ b/source/extensions/omni.isaac.lab/test/deps/isaacsim/check_floating_base_made_fixed.py @@ -32,11 +32,11 @@ import torch -import carb import omni.isaac.core.utils.nucleus as nucleus_utils import omni.isaac.core.utils.prims as prim_utils import omni.isaac.core.utils.stage as stage_utils import omni.kit.commands +import omni.log import omni.physx from omni.isaac.core.articulations import ArticulationView from omni.isaac.core.utils.carb import set_carb_setting @@ -50,7 +50,7 @@ "Unable to perform Nucleus login on Omniverse. Assets root path is not set.\n" "\tPlease check: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/overview.html#omniverse-nucleus" ) - carb.log_error(msg) + omni.log.error(msg) raise RuntimeError(msg) diff --git a/source/extensions/omni.isaac.lab/test/deps/isaacsim/check_legged_robot_clone.py b/source/extensions/omni.isaac.lab/test/deps/isaacsim/check_legged_robot_clone.py index 628872496b..8edc29071d 100644 --- a/source/extensions/omni.isaac.lab/test/deps/isaacsim/check_legged_robot_clone.py +++ b/source/extensions/omni.isaac.lab/test/deps/isaacsim/check_legged_robot_clone.py @@ -43,7 +43,7 @@ import os import torch -import carb +import omni.log try: import omni.isaac.nucleus as nucleus_utils @@ -62,7 +62,7 @@ "Unable to perform Nucleus login on Omniverse. Assets root path is not set.\n" "\tPlease check: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/overview.html#omniverse-nucleus" ) - carb.log_error(msg) + omni.log.error(msg) raise RuntimeError(msg) diff --git a/source/extensions/omni.isaac.lab/test/deps/isaacsim/check_ref_count.py b/source/extensions/omni.isaac.lab/test/deps/isaacsim/check_ref_count.py index 7d2e91acd7..49bf581f27 100644 --- a/source/extensions/omni.isaac.lab/test/deps/isaacsim/check_ref_count.py +++ b/source/extensions/omni.isaac.lab/test/deps/isaacsim/check_ref_count.py @@ -37,7 +37,7 @@ import gc import torch # noqa: F401 -import carb +import omni.log try: import omni.isaac.nucleus as nucleus_utils @@ -55,7 +55,7 @@ "Unable to perform Nucleus login on Omniverse. Assets root path is not set.\n" "\tPlease check: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/overview.html#omniverse-nucleus" ) - carb.log_error(msg) + omni.log.error(msg) raise RuntimeError(msg) diff --git a/source/extensions/omni.isaac.lab/test/envs/test_action_state_recorder_term.py b/source/extensions/omni.isaac.lab/test/envs/test_action_state_recorder_term.py new file mode 100644 index 0000000000..d3640f63ba --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/envs/test_action_state_recorder_term.py @@ -0,0 +1,123 @@ +# Copyright (c) 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 the simulator +app_launcher = AppLauncher(headless=True) +simulation_app = app_launcher.app + + +"""Rest everything follows.""" + +import gymnasium as gym +import shutil +import tempfile +import torch +import unittest +import uuid + +import carb +import omni.usd + +from omni.isaac.lab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg + +import omni.isaac.lab_tasks # noqa: F401 +from omni.isaac.lab_tasks.utils.parse_cfg import parse_env_cfg + + +class TestActionStateRecorderManagerCfg(unittest.TestCase): + """Test cases for ActionStateRecorderManagerCfg recorder terms.""" + + @classmethod + def setUpClass(cls): + # this flag is necessary to prevent a bug where the simulation gets stuck randomly when running the + # test on many environments. + carb_settings_iface = carb.settings.get_settings() + carb_settings_iface.set_bool("/physics/cooking/ujitsoCollisionCooking", False) + + def setUp(self): + # create a temporary directory to store the test datasets + self.temp_dir = tempfile.mkdtemp() + + def tearDown(self): + # delete the temporary directory after the test + shutil.rmtree(self.temp_dir) + + def test_action_state_reocrder_terms(self): + """Check action state recorder terms.""" + for task_name in ["Isaac-Lift-Cube-Franka-v0"]: + for device in ["cuda:0", "cpu"]: + for num_envs in [1, 2]: + with self.subTest(task_name=task_name, device=device): + omni.usd.get_context().new_stage() + + dummy_dataset_filename = f"{uuid.uuid4()}.hdf5" + + # parse configuration + env_cfg = parse_env_cfg(task_name, device=device, num_envs=num_envs) + # set recorder configurations for this test + env_cfg.recorders: ActionStateRecorderManagerCfg = ActionStateRecorderManagerCfg() + env_cfg.recorders.dataset_export_dir_path = self.temp_dir + env_cfg.recorders.dataset_filename = dummy_dataset_filename + + # create environment + env = gym.make(task_name, cfg=env_cfg) + + # reset all environment instances to trigger post-reset recorder callbacks + env.reset() + self.check_initial_state_recorder_term(env) + + # reset only one environment that is not the first one + env.unwrapped.reset(env_ids=torch.tensor([num_envs - 1], device=env.unwrapped.device)) + self.check_initial_state_recorder_term(env) + + # close the environment + env.close() + + def check_initial_state_recorder_term(self, env): + """Check values recorded by the initial state recorder terms. + + Args: + env: Environment instance. + """ + current_state = env.unwrapped.scene.get_state(is_relative=True) + for env_id in range(env.unwrapped.num_envs): + recorded_initial_state = env.unwrapped.recorder_manager.get_episode(env_id).get_initial_state() + are_states_equal, output_log = self.compare_states(recorded_initial_state, current_state, env_id) + self.assertTrue(are_states_equal, msg=output_log) + + def compare_states(self, compared_state, ground_truth_state, ground_truth_env_id) -> (bool, str): + """Compare a state with the given ground_truth. + + Args: + compared_state: State to be compared. + ground_truth_state: Ground truth state. + ground_truth_env_id: Index of the environment in the ground_truth states to be compared. + + Returns: + bool: True if states match, False otherwise. + str: Error log if states don't match. + """ + for asset_type in ["articulation", "rigid_object"]: + for asset_name in ground_truth_state[asset_type].keys(): + for state_name in ground_truth_state[asset_type][asset_name].keys(): + runtime_asset_state = ground_truth_state[asset_type][asset_name][state_name][ground_truth_env_id] + dataset_asset_state = compared_state[asset_type][asset_name][state_name][0] + if len(dataset_asset_state) != len(runtime_asset_state): + return False, f"State shape of {state_name} for asset {asset_name} don't match" + for i in range(len(dataset_asset_state)): + if abs(dataset_asset_state[i] - runtime_asset_state[i]) > 0.01: + return ( + False, + f'State ["{asset_type}"]["{asset_name}"]["{state_name}"][{i}] don\'t match\r\n', + ) + return True, "" + + +if __name__ == "__main__": + run_tests() 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 ec6cb971c2..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 @@ -21,7 +21,6 @@ """Rest everything follows.""" -import torch import unittest import omni.usd @@ -50,9 +49,10 @@ class EmptyEnvCfg(DirectMARLEnvCfg): # Basic settings decimation = 1 possible_agents = ["agent_0", "agent_1"] - num_actions = {"agent_0": 1, "agent_1": 2} - num_observations = {"agent_0": 3, "agent_1": 4} - num_states = -1 + 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() @@ -69,8 +69,17 @@ def test_initialization(self): with self.subTest(device=device): # create a new stage omni.usd.get_context().new_stage() - # create environment - env = DirectMARLEnv(cfg=get_empty_base_env_cfg(device=device)) + try: + # create environment + env = DirectMARLEnv(cfg=get_empty_base_env_cfg(device=device)) + 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 DirectMARLEnv environment. Error: {e}") + # check multi-agent config self.assertEqual(env.num_agents, 2) self.assertEqual(env.max_num_agents, 2) @@ -78,12 +87,6 @@ def test_initialization(self): self.assertEqual(env.state_space.shape, (7,)) self.assertEqual(len(env.observation_spaces), 2) self.assertEqual(len(env.action_spaces), 2) - # step environment to verify setup - env.reset() - for _ in range(2): - actions = {"agent_0": torch.rand((1, 1)), "agent_1": torch.rand((1, 2))} - obs, reward, terminated, truncate, info = env.step(actions) - env.state() # close the environment env.close() 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 24af5c91d6..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()) @@ -79,8 +83,9 @@ class EnvCfg(DirectRLEnvCfg): """Configuration for the test environment.""" decimation: int = 4 - num_actions: int = 0 - num_observations: int = 0 + 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) @@ -131,14 +136,21 @@ def test_env_rendering_logic(self): # create a new stage omni.usd.get_context().new_stage() - - # create environment - if env_type == "manager_based_env": - env = create_manager_based_env(render_interval) - elif env_type == "manager_based_rl_env": - env = create_manager_based_rl_env(render_interval) - else: - env = create_direct_rl_env(render_interval) + try: + # create environment + if env_type == "manager_based_env": + env = create_manager_based_env(render_interval) + elif env_type == "manager_based_rl_env": + env = create_manager_based_rl_env(render_interval) + else: + env = create_direct_rl_env(render_interval) + 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 {env_type}. Error: {e}") # enable the flag to render the environment # note: this is only done for the unit testing to "fake" camera rendering. diff --git a/source/extensions/omni.isaac.lab/test/envs/test_manager_based_rl_env_ui.py b/source/extensions/omni.isaac.lab/test/envs/test_manager_based_rl_env_ui.py new file mode 100644 index 0000000000..f61af77b8d --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/envs/test_manager_based_rl_env_ui.py @@ -0,0 +1,104 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# ignore private usage of variables warning +# pyright: reportPrivateUsage=none + +from __future__ import annotations + +"""Launch Isaac Sim Simulator first.""" + +from omni.isaac.lab.app import AppLauncher, run_tests + +# Can set this to False to see the GUI for debugging +HEADLESS = True + +# launch omniverse app +app_launcher = AppLauncher(headless=HEADLESS, enable_cameras=True) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import unittest + +import carb +import omni.usd +from omni.isaac.core.utils.extensions import enable_extension + +from omni.isaac.lab.envs import ManagerBasedRLEnv, ManagerBasedRLEnvCfg +from omni.isaac.lab.envs.ui import ManagerBasedRLEnvWindow +from omni.isaac.lab.scene import InteractiveSceneCfg +from omni.isaac.lab.utils import configclass + +enable_extension("omni.isaac.ui") + + +@configclass +class EmptyManagerCfg: + """Empty manager specifications for the environment.""" + + pass + + +@configclass +class EmptySceneCfg(InteractiveSceneCfg): + """Configuration for an empty scene.""" + + pass + + +def get_empty_base_env_cfg(device: str = "cuda:0", num_envs: int = 1, env_spacing: float = 1.0): + """Generate base environment config based on device""" + + @configclass + class EmptyEnvCfg(ManagerBasedRLEnvCfg): + """Configuration for the empty test environment.""" + + # Scene settings + scene: EmptySceneCfg = EmptySceneCfg(num_envs=num_envs, env_spacing=env_spacing) + # Basic settings + actions: EmptyManagerCfg = EmptyManagerCfg() + observations: EmptyManagerCfg = EmptyManagerCfg() + rewards: EmptyManagerCfg = EmptyManagerCfg() + terminations: EmptyManagerCfg = EmptyManagerCfg() + # Define window + ui_window_class_type: ManagerBasedRLEnvWindow = ManagerBasedRLEnvWindow + + def __post_init__(self): + """Post initialization.""" + # step settings + self.decimation = 4 # env step every 4 sim steps: 200Hz / 4 = 50Hz + # simulation settings + self.sim.dt = 0.005 # sim step every 5ms: 200Hz + self.sim.render_interval = self.decimation # render every 4 sim steps + # pass device down from test + self.sim.device = device + # episode length + self.episode_length_s = 5.0 + + return EmptyEnvCfg() + + +class TestManagerBasedRLEnvUI(unittest.TestCase): + """Test for manager-based RL env class UI""" + + """ + Tests + """ + + def test_ui_window(self): + device = "cuda:0" + # override sim setting to enable UI + carb.settings.get_settings().set_bool("/app/window/enabled", True) + # create a new stage + omni.usd.get_context().new_stage() + # create environment + env = ManagerBasedRLEnv(cfg=get_empty_base_env_cfg(device=device)) + # close the environment + env.close() + + +if __name__ == "__main__": + run_tests() diff --git a/source/extensions/omni.isaac.lab/test/envs/test_spaces_utils.py b/source/extensions/omni.isaac.lab/test/envs/test_spaces_utils.py new file mode 100644 index 0000000000..791c61380c --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/envs/test_spaces_utils.py @@ -0,0 +1,175 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# ignore private usage of variables warning +# pyright: reportPrivateUsage=none + +from __future__ import annotations + +"""Launch Isaac Sim Simulator first.""" + +from omni.isaac.lab.app import AppLauncher, run_tests + +# Can set this to False to see the GUI for debugging +HEADLESS = True + +# launch omniverse app +app_launcher = AppLauncher(headless=HEADLESS) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import numpy as np +import torch +import unittest +from gymnasium.spaces import Box, Dict, Discrete, MultiDiscrete, Tuple + +from omni.isaac.lab.envs.utils.spaces import deserialize_space, sample_space, serialize_space, spec_to_gym_space + + +class TestSpacesUtils(unittest.TestCase): + """Test for spaces utils' functions""" + + """ + Tests + """ + + def test_spec_to_gym_space(self): + # fundamental spaces + # Box + space = spec_to_gym_space(1) + self.assertIsInstance(space, Box) + self.assertEqual(space.shape, (1,)) + space = spec_to_gym_space([1, 2, 3, 4, 5]) + self.assertIsInstance(space, Box) + self.assertEqual(space.shape, (1, 2, 3, 4, 5)) + space = spec_to_gym_space(Box(low=-1.0, high=1.0, shape=(1, 2))) + self.assertIsInstance(space, Box) + # Discrete + space = spec_to_gym_space({2}) + self.assertIsInstance(space, Discrete) + self.assertEqual(space.n, 2) + space = spec_to_gym_space(Discrete(2)) + self.assertIsInstance(space, Discrete) + # MultiDiscrete + space = spec_to_gym_space([{1}, {2}, {3}]) + self.assertIsInstance(space, MultiDiscrete) + self.assertEqual(space.nvec.shape, (3,)) + space = spec_to_gym_space(MultiDiscrete(np.array([1, 2, 3]))) + self.assertIsInstance(space, MultiDiscrete) + # composite spaces + # Tuple + space = spec_to_gym_space(([1, 2, 3, 4, 5], {2}, [{1}, {2}, {3}])) + self.assertIsInstance(space, Tuple) + self.assertEqual(len(space), 3) + self.assertIsInstance(space[0], Box) + self.assertIsInstance(space[1], Discrete) + self.assertIsInstance(space[2], MultiDiscrete) + space = spec_to_gym_space(Tuple((Box(-1, 1, shape=(1,)), Discrete(2)))) + self.assertIsInstance(space, Tuple) + # Dict + space = spec_to_gym_space({"box": [1, 2, 3, 4, 5], "discrete": {2}, "multi_discrete": [{1}, {2}, {3}]}) + self.assertIsInstance(space, Dict) + self.assertEqual(len(space), 3) + self.assertIsInstance(space["box"], Box) + self.assertIsInstance(space["discrete"], Discrete) + self.assertIsInstance(space["multi_discrete"], MultiDiscrete) + space = spec_to_gym_space(Dict({"box": Box(-1, 1, shape=(1,)), "discrete": Discrete(2)})) + self.assertIsInstance(space, Dict) + + def test_sample_space(self): + device = "cpu" + # fundamental spaces + # Box + sample = sample_space(Box(low=-1.0, high=1.0, shape=(1, 2)), device, batch_size=1) + self.assertIsInstance(sample, torch.Tensor) + self._check_tensorized(sample, batch_size=1) + # Discrete + sample = sample_space(Discrete(2), device, batch_size=2) + self.assertIsInstance(sample, torch.Tensor) + self._check_tensorized(sample, batch_size=2) + # MultiDiscrete + sample = sample_space(MultiDiscrete(np.array([1, 2, 3])), device, batch_size=3) + self.assertIsInstance(sample, torch.Tensor) + self._check_tensorized(sample, batch_size=3) + # composite spaces + # Tuple + sample = sample_space(Tuple((Box(-1, 1, shape=(1,)), Discrete(2))), device, batch_size=4) + self.assertIsInstance(sample, (tuple, list)) + self._check_tensorized(sample, batch_size=4) + # Dict + sample = sample_space(Dict({"box": Box(-1, 1, shape=(1,)), "discrete": Discrete(2)}), device, batch_size=5) + self.assertIsInstance(sample, dict) + self._check_tensorized(sample, batch_size=5) + + def test_space_serialization_deserialization(self): + # fundamental spaces + # Box + space = 1 + output = deserialize_space(serialize_space(space)) + self.assertEqual(space, output) + space = [1, 2, 3, 4, 5] + output = deserialize_space(serialize_space(space)) + self.assertEqual(space, output) + space = Box(low=-1.0, high=1.0, shape=(1, 2)) + output = deserialize_space(serialize_space(space)) + self.assertIsInstance(output, Box) + self.assertTrue((space.low == output.low).all()) + self.assertTrue((space.high == output.high).all()) + self.assertEqual(space.shape, output.shape) + # Discrete + space = {2} + output = deserialize_space(serialize_space(space)) + self.assertEqual(space, output) + space = Discrete(2) + output = deserialize_space(serialize_space(space)) + self.assertIsInstance(output, Discrete) + self.assertEqual(space.n, output.n) + # MultiDiscrete + space = [{1}, {2}, {3}] + output = deserialize_space(serialize_space(space)) + self.assertEqual(space, output) + space = MultiDiscrete(np.array([1, 2, 3])) + output = deserialize_space(serialize_space(space)) + self.assertIsInstance(output, MultiDiscrete) + self.assertTrue((space.nvec == output.nvec).all()) + # composite spaces + # Tuple + space = ([1, 2, 3, 4, 5], {2}, [{1}, {2}, {3}]) + output = deserialize_space(serialize_space(space)) + self.assertEqual(space, output) + space = Tuple((Box(-1, 1, shape=(1,)), Discrete(2))) + output = deserialize_space(serialize_space(space)) + self.assertIsInstance(output, Tuple) + self.assertEqual(len(output), 2) + self.assertIsInstance(output[0], Box) + self.assertIsInstance(output[1], Discrete) + # Dict + space = {"box": [1, 2, 3, 4, 5], "discrete": {2}, "multi_discrete": [{1}, {2}, {3}]} + output = deserialize_space(serialize_space(space)) + self.assertEqual(space, output) + space = Dict({"box": Box(-1, 1, shape=(1,)), "discrete": Discrete(2)}) + output = deserialize_space(serialize_space(space)) + self.assertIsInstance(output, Dict) + self.assertEqual(len(output), 2) + self.assertIsInstance(output["box"], Box) + self.assertIsInstance(output["discrete"], Discrete) + + """ + Helper functions. + """ + + def _check_tensorized(self, sample, batch_size): + if isinstance(sample, (tuple, list)): + list(map(self._check_tensorized, sample, [batch_size] * len(sample))) + elif isinstance(sample, dict): + list(map(self._check_tensorized, sample.values(), [batch_size] * len(sample))) + else: + self.assertIsInstance(sample, torch.Tensor) + self.assertEqual(sample.shape[0], batch_size) + + +if __name__ == "__main__": + run_tests() 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_observation_manager.py b/source/extensions/omni.isaac.lab/test/managers/test_observation_manager.py index 9b73e2d44a..c624fb2bd1 100644 --- a/source/extensions/omni.isaac.lab/test/managers/test_observation_manager.py +++ b/source/extensions/omni.isaac.lab/test/managers/test_observation_manager.py @@ -244,6 +244,8 @@ class SampleImageGroupCfg(ObservationGroupCfg): def test_compute(self): """Test the observation computation.""" + pos_scale_tuple = (2.0, 3.0, 1.0) + @configclass class MyObservationManagerCfg: """Test config class for observation manager.""" @@ -254,14 +256,14 @@ class PolicyCfg(ObservationGroupCfg): term_1 = ObservationTermCfg(func=grilled_chicken, scale=10) term_2 = ObservationTermCfg(func=grilled_chicken_with_curry, scale=0.0, params={"hot": False}) - term_3 = ObservationTermCfg(func=pos_w_data, scale=2.0) + term_3 = ObservationTermCfg(func=pos_w_data, scale=pos_scale_tuple) term_4 = ObservationTermCfg(func=lin_vel_w_data, scale=1.5) @configclass class CriticCfg(ObservationGroupCfg): - term_1 = ObservationTermCfg(func=pos_w_data, scale=2.0) + term_1 = ObservationTermCfg(func=pos_w_data, scale=pos_scale_tuple) term_2 = ObservationTermCfg(func=lin_vel_w_data, scale=1.5) - term_3 = ObservationTermCfg(func=pos_w_data, scale=2.0) + term_3 = ObservationTermCfg(func=pos_w_data, scale=pos_scale_tuple) term_4 = ObservationTermCfg(func=lin_vel_w_data, scale=1.5) @configclass @@ -289,6 +291,11 @@ class ImageCfg(ObservationGroupCfg): self.assertEqual((self.env.num_envs, 11), obs_policy.shape) self.assertEqual((self.env.num_envs, 12), obs_critic.shape) self.assertEqual((self.env.num_envs, 128, 256, 4), obs_image.shape) + # check that the scales are applied correctly + torch.testing.assert_close( + self.env.data.pos_w * torch.tensor(pos_scale_tuple, device=self.env.device), obs_critic[:, :3] + ) + torch.testing.assert_close(self.env.data.lin_vel_w * 1.5, obs_critic[:, 3:6]) # make sure that the data are the same for same terms # -- within group torch.testing.assert_close(obs_critic[:, 0:3], obs_critic[:, 6:9]) diff --git a/source/extensions/omni.isaac.lab/test/managers/test_recorder_manager.py b/source/extensions/omni.isaac.lab/test/managers/test_recorder_manager.py new file mode 100644 index 0000000000..594482d6da --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/managers/test_recorder_manager.py @@ -0,0 +1,168 @@ +# Copyright (c) 2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# needed to import for allowing type-hinting: torch.Tensor | None +from __future__ import annotations + +"""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 os +import shutil +import tempfile +import torch +import unittest +import uuid +from collections import namedtuple +from collections.abc import Sequence + +from omni.isaac.lab.envs import ManagerBasedEnv +from omni.isaac.lab.managers import ( + DatasetExportMode, + RecorderManager, + RecorderManagerBaseCfg, + RecorderTerm, + RecorderTermCfg, +) +from omni.isaac.lab.utils import configclass + + +class DummyResetRecorderTerm(RecorderTerm): + """Dummy recorder term that records dummy data.""" + + def __init__(self, cfg: RecorderTermCfg, env: ManagerBasedEnv) -> None: + super().__init__(cfg, env) + + def record_pre_reset(self, env_ids: Sequence[int] | None) -> tuple[str | None, torch.Tensor | None]: + return "record_pre_reset", torch.ones(self._env.num_envs, 2, device=self._env.device) + + def record_post_reset(self, env_ids: Sequence[int] | None) -> tuple[str | None, torch.Tensor | None]: + return "record_post_reset", torch.ones(self._env.num_envs, 3, device=self._env.device) + + +class DummyStepRecorderTerm(RecorderTerm): + """Dummy recorder term that records dummy data.""" + + def __init__(self, cfg: RecorderTermCfg, env: ManagerBasedEnv) -> None: + super().__init__(cfg, env) + + def record_pre_step(self) -> tuple[str | None, torch.Tensor | None]: + return "record_pre_step", torch.ones(self._env.num_envs, 4, device=self._env.device) + + def record_post_step(self) -> tuple[str | None, torch.Tensor | None]: + return "record_post_step", torch.ones(self._env.num_envs, 5, device=self._env.device) + + +@configclass +class DummyRecorderManagerCfg(RecorderManagerBaseCfg): + """Dummy recorder configurations.""" + + @configclass + class DummyResetRecorderTermCfg(RecorderTermCfg): + """Configuration for the dummy reset recorder term.""" + + class_type: type[RecorderTerm] = DummyResetRecorderTerm + + @configclass + class DummyStepRecorderTermCfg(RecorderTermCfg): + """Configuration for the dummy step recorder term.""" + + class_type: type[RecorderTerm] = DummyStepRecorderTerm + + record_reset_term = DummyResetRecorderTermCfg() + record_step_term = DummyStepRecorderTermCfg() + + dataset_export_mode = DatasetExportMode.EXPORT_ALL + + +def create_dummy_env(device: str = "cpu") -> ManagerBasedEnv: + """Create a dummy environment.""" + + class DummyTerminationManager: + active_terms = [] + + dummy_termination_manager = DummyTerminationManager() + return namedtuple("ManagerBasedEnv", ["num_envs", "device", "cfg", "termination_manager"])( + 20, device, dict(), dummy_termination_manager + ) + + +class TestRecorderManager(unittest.TestCase): + """Test cases for various situations with recorder manager.""" + + def setUp(self) -> None: + self.dataset_dir = tempfile.mkdtemp() + + def tearDown(self): + # delete the temporary directory after the test + shutil.rmtree(self.dataset_dir) + + def create_dummy_recorder_manager_cfg(self) -> DummyRecorderManagerCfg: + """Get the dummy recorder manager configurations.""" + cfg = DummyRecorderManagerCfg() + cfg.dataset_export_dir_path = self.dataset_dir + cfg.dataset_filename = f"{uuid.uuid4()}.hdf5" + return cfg + + def test_str(self): + """Test the string representation of the recorder manager.""" + # create recorder manager + cfg = DummyRecorderManagerCfg() + recorder_manager = RecorderManager(cfg, create_dummy_env()) + self.assertEqual(len(recorder_manager.active_terms), 2) + # print the expected string + print() + print(recorder_manager) + + def test_initialize_dataset_file(self): + """Test the initialization of the dataset file.""" + # create recorder manager + cfg = self.create_dummy_recorder_manager_cfg() + _ = RecorderManager(cfg, create_dummy_env()) + + # check if the dataset is created + self.assertTrue(os.path.exists(os.path.join(cfg.dataset_export_dir_path, cfg.dataset_filename))) + + def test_record(self): + """Test the recording of the data.""" + for device in ("cuda:0", "cpu"): + with self.subTest(device=device): + env = create_dummy_env(device) + # create recorder manager + recorder_manager = RecorderManager(self.create_dummy_recorder_manager_cfg(), env) + + # record the step data + recorder_manager.record_pre_step() + recorder_manager.record_post_step() + + recorder_manager.record_pre_step() + recorder_manager.record_post_step() + + # check the recorded data + for env_id in range(env.num_envs): + episode = recorder_manager.get_episode(env_id) + self.assertEqual(episode.data["record_pre_step"].shape, (2, 4)) + self.assertEqual(episode.data["record_post_step"].shape, (2, 5)) + + # Trigger pre-reset callbacks which then export and clean the episode data + recorder_manager.record_pre_reset(env_ids=None) + for env_id in range(env.num_envs): + episode = recorder_manager.get_episode(env_id) + self.assertTrue(episode.is_empty()) + + recorder_manager.record_post_reset(env_ids=None) + for env_id in range(env.num_envs): + episode = recorder_manager.get_episode(env_id) + self.assertEqual(episode.data["record_post_reset"].shape, (1, 3)) + + +if __name__ == "__main__": + run_tests() 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/sensors/check_imu_sensor.py b/source/extensions/omni.isaac.lab/test/sensors/check_imu_sensor.py new file mode 100644 index 0000000000..6516c0eefb --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/sensors/check_imu_sensor.py @@ -0,0 +1,196 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Visual test script for the imu sensor from the Orbit framework. +""" + +from __future__ import annotations + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +from omni.isaac.kit import SimulationApp + +# add argparse arguments +parser = argparse.ArgumentParser(description="Imu Test Script") +parser.add_argument("--headless", action="store_true", default=False, help="Force display off at all times.") +parser.add_argument("--num_envs", type=int, default=128, help="Number of environments to clone.") +parser.add_argument( + "--terrain_type", + type=str, + default="generator", + choices=["generator", "usd", "plane"], + help="Type of terrain to import. Can be 'generator' or 'usd' or 'plane'.", +) +args_cli = parser.parse_args() + +# launch omniverse app +config = {"headless": args_cli.headless} +simulation_app = SimulationApp(config) + + +"""Rest everything follows.""" + +import torch +import traceback + +import carb +import omni +from omni.isaac.cloner import GridCloner +from omni.isaac.core.simulation_context import SimulationContext +from omni.isaac.core.utils.viewports import set_camera_view +from pxr import PhysxSchema + +import omni.isaac.lab.sim as sim_utils +import omni.isaac.lab.terrains as terrain_gen +from omni.isaac.lab.assets import RigidObject, RigidObjectCfg +from omni.isaac.lab.sensors.imu import Imu, ImuCfg +from omni.isaac.lab.terrains.config.rough import ROUGH_TERRAINS_CFG +from omni.isaac.lab.terrains.terrain_importer import TerrainImporter +from omni.isaac.lab.utils.assets import ISAAC_NUCLEUS_DIR +from omni.isaac.lab.utils.timer import Timer + + +def design_scene(sim: SimulationContext, num_envs: int = 2048) -> RigidObject: + """Design the scene.""" + # Handler for terrains importing + terrain_importer_cfg = terrain_gen.TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="generator", + terrain_generator=ROUGH_TERRAINS_CFG, + usd_path=f"{ISAAC_NUCLEUS_DIR}/Environments/Terrains/rough_plane.usd", + max_init_terrain_level=None, + num_envs=1, + ) + _ = TerrainImporter(terrain_importer_cfg) + # obtain the current stage + stage = omni.usd.get_context().get_stage() + # Create interface to clone the scene + cloner = GridCloner(spacing=2.0) + cloner.define_base_env("/World/envs") + envs_prim_paths = cloner.generate_paths("/World/envs/env", num_paths=num_envs) + # create source prim + stage.DefinePrim(envs_prim_paths[0], "Xform") + # clone the env xform + cloner.clone(source_prim_path="/World/envs/env_0", prim_paths=envs_prim_paths, replicate_physics=True) + # Define the scene + # -- Light + cfg = sim_utils.DistantLightCfg(intensity=2000) + cfg.func("/World/light", cfg) + # -- Balls + cfg = RigidObjectCfg( + spawn=sim_utils.SphereCfg( + radius=0.25, + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + collision_props=sim_utils.CollisionPropertiesCfg(), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0)), + ), + prim_path="/World/envs/env_.*/ball", + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 5.0)), + ) + balls = RigidObject(cfg) + # Clone the scene + # obtain the current physics scene + physics_scene_prim_path = None + for prim in stage.Traverse(): + if prim.HasAPI(PhysxSchema.PhysxSceneAPI): + physics_scene_prim_path = prim.GetPrimPath() + carb.log_info(f"Physics scene prim path: {physics_scene_prim_path}") + break + # filter collisions within each environment instance + cloner.filter_collisions( + physics_scene_prim_path, + "/World/collisions", + envs_prim_paths, + ) + return balls + + +def main(): + """Main function.""" + + # Load kit helper + sim_params = { + "use_gpu": True, + "use_gpu_pipeline": True, + "use_flatcache": True, # deprecated from Isaac Sim 2023.1 onwards + "use_fabric": True, # used from Isaac Sim 2023.1 onwards + "enable_scene_query_support": True, + } + sim = SimulationContext( + physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, sim_params=sim_params, backend="torch", device="cuda:0" + ) + # Set main camera + set_camera_view([0.0, 30.0, 25.0], [0.0, 0.0, -2.5]) + + # Parameters + num_envs = args_cli.num_envs + # Design the scene + balls = design_scene(sim=sim, num_envs=num_envs) + + # Create a ray-caster sensor + imu_cfg = ImuCfg( + prim_path="/World/envs/env_.*/ball", + debug_vis=not args_cli.headless, + ) + # increase scale of the arrows for better visualization + imu_cfg.visualizer_cfg.markers["arrow"].scale = (1.0, 0.2, 0.2) + imu = Imu(cfg=imu_cfg) + + # Play simulator and init the Imu + sim.reset() + + # Print the sensor information + print(imu) + + # Get the ball initial positions + sim.step(render=not args_cli.headless) + balls.update(sim.get_physics_dt()) + ball_initial_positions = balls.data.root_pos_w.clone() + ball_initial_orientations = balls.data.root_quat_w.clone() + + # Create a counter for resetting the scene + step_count = 0 + # Simulate physics + while simulation_app.is_running(): + # If simulation is stopped, then exit. + if sim.is_stopped(): + break + # If simulation is paused, then skip. + if not sim.is_playing(): + sim.step(render=not args_cli.headless) + continue + # Reset the scene + if step_count % 500 == 0: + # reset ball positions + balls.write_root_pose_to_sim(torch.cat([ball_initial_positions, ball_initial_orientations], dim=-1)) + balls.reset() + # reset the sensor + imu.reset() + # reset the counter + step_count = 0 + # Step simulation + sim.step() + # Update the imu sensor + with Timer(f"Imu sensor update with {num_envs}"): + imu.update(dt=sim.get_physics_dt(), force_recompute=True) + # Update counter + step_count += 1 + + +if __name__ == "__main__": + try: + # Run the main function + main() + except Exception as err: + carb.log_error(err) + carb.log_error(traceback.format_exc()) + raise + finally: + # close sim app + simulation_app.close() diff --git a/source/extensions/omni.isaac.lab/test/sensors/test_camera.py b/source/extensions/omni.isaac.lab/test/sensors/test_camera.py index fc6953e166..fad7d5893d 100644 --- a/source/extensions/omni.isaac.lab/test/sensors/test_camera.py +++ b/source/extensions/omni.isaac.lab/test/sensors/test_camera.py @@ -121,7 +121,7 @@ def test_camera_init(self): # update camera camera.update(self.dt) # check image data - for im_data in camera.data.output.to_dict().values(): + for im_data in camera.data.output.values(): self.assertEqual(im_data.shape, (1, self.camera_cfg.height, self.camera_cfg.width, 1)) def test_camera_init_offset(self): @@ -228,7 +228,7 @@ def test_multi_camera_init(self): cam_2.update(self.dt) # check image data for cam in [cam_1, cam_2]: - for im_data in cam.data.output.to_dict().values(): + for im_data in cam.data.output.values(): self.assertEqual(im_data.shape, (1, self.camera_cfg.height, self.camera_cfg.width, 1)) def test_multi_camera_with_different_resolution(self): @@ -393,6 +393,137 @@ def test_intrinsic_matrix(self): torch.testing.assert_close(rs_intrinsic_matrix[0, 0, 0], camera.data.intrinsic_matrices[0, 0, 0]) # torch.testing.assert_close(rs_intrinsic_matrix[0, 1, 1], camera.data.intrinsic_matrices[0, 1, 1]) + def test_depth_clipping(self): + """Test depth clipping. + + .. note:: + + This test is the same for all camera models to enforce the same clipping behavior. + """ + # get camera cfgs + camera_cfg_zero = CameraCfg( + prim_path="/World/CameraZero", + offset=CameraCfg.OffsetCfg(pos=(2.5, 2.5, 6.0), rot=(-0.125, 0.362, 0.873, -0.302), convention="ros"), + spawn=sim_utils.PinholeCameraCfg().from_intrinsic_matrix( + focal_length=38.0, + intrinsic_matrix=[380.08, 0.0, 467.79, 0.0, 380.08, 262.05, 0.0, 0.0, 1.0], + height=540, + width=960, + clipping_range=(0.1, 10), + ), + height=540, + width=960, + data_types=["distance_to_image_plane", "distance_to_camera"], + depth_clipping_behavior="zero", + ) + camera_zero = Camera(camera_cfg_zero) + + camera_cfg_none = copy.deepcopy(camera_cfg_zero) + camera_cfg_none.prim_path = "/World/CameraNone" + camera_cfg_none.depth_clipping_behavior = "none" + camera_none = Camera(camera_cfg_none) + + camera_cfg_max = copy.deepcopy(camera_cfg_zero) + camera_cfg_max.prim_path = "/World/CameraMax" + camera_cfg_max.depth_clipping_behavior = "max" + camera_max = Camera(camera_cfg_max) + + # Play sim + self.sim.reset() + + # note: This is a workaround to ensure that the textures are loaded. + # Check "Known Issues" section in the documentation for more details. + for _ in range(5): + self.sim.step() + + camera_zero.update(self.dt) + camera_none.update(self.dt) + camera_max.update(self.dt) + + # none clipping should contain inf values + self.assertTrue(torch.isinf(camera_none.data.output["distance_to_camera"]).any()) + self.assertTrue(torch.isinf(camera_none.data.output["distance_to_image_plane"]).any()) + self.assertTrue( + camera_none.data.output["distance_to_camera"][ + ~torch.isinf(camera_none.data.output["distance_to_camera"]) + ].min() + >= camera_cfg_zero.spawn.clipping_range[0] + ) + self.assertTrue( + camera_none.data.output["distance_to_camera"][ + ~torch.isinf(camera_none.data.output["distance_to_camera"]) + ].max() + <= camera_cfg_zero.spawn.clipping_range[1] + ) + self.assertTrue( + camera_none.data.output["distance_to_image_plane"][ + ~torch.isinf(camera_none.data.output["distance_to_image_plane"]) + ].min() + >= camera_cfg_zero.spawn.clipping_range[0] + ) + self.assertTrue( + camera_none.data.output["distance_to_image_plane"][ + ~torch.isinf(camera_none.data.output["distance_to_camera"]) + ].max() + <= camera_cfg_zero.spawn.clipping_range[1] + ) + + # zero clipping should result in zero values + self.assertTrue( + torch.all( + camera_zero.data.output["distance_to_camera"][ + torch.isinf(camera_none.data.output["distance_to_camera"]) + ] + == 0.0 + ) + ) + self.assertTrue( + torch.all( + camera_zero.data.output["distance_to_image_plane"][ + torch.isinf(camera_none.data.output["distance_to_image_plane"]) + ] + == 0.0 + ) + ) + self.assertTrue( + camera_zero.data.output["distance_to_camera"][camera_zero.data.output["distance_to_camera"] != 0.0].min() + >= camera_cfg_zero.spawn.clipping_range[0] + ) + self.assertTrue(camera_zero.data.output["distance_to_camera"].max() <= camera_cfg_zero.spawn.clipping_range[1]) + self.assertTrue( + camera_zero.data.output["distance_to_image_plane"][ + camera_zero.data.output["distance_to_image_plane"] != 0.0 + ].min() + >= camera_cfg_zero.spawn.clipping_range[0] + ) + self.assertTrue( + camera_zero.data.output["distance_to_image_plane"].max() <= camera_cfg_zero.spawn.clipping_range[1] + ) + + # max clipping should result in max values + self.assertTrue( + torch.all( + camera_max.data.output["distance_to_camera"][torch.isinf(camera_none.data.output["distance_to_camera"])] + == camera_cfg_zero.spawn.clipping_range[1] + ) + ) + self.assertTrue( + torch.all( + camera_max.data.output["distance_to_image_plane"][ + torch.isinf(camera_none.data.output["distance_to_image_plane"]) + ] + == camera_cfg_zero.spawn.clipping_range[1] + ) + ) + self.assertTrue(camera_max.data.output["distance_to_camera"].min() >= camera_cfg_zero.spawn.clipping_range[0]) + self.assertTrue(camera_max.data.output["distance_to_camera"].max() <= camera_cfg_zero.spawn.clipping_range[1]) + self.assertTrue( + camera_max.data.output["distance_to_image_plane"].min() >= camera_cfg_zero.spawn.clipping_range[0] + ) + self.assertTrue( + camera_max.data.output["distance_to_image_plane"].max() <= camera_cfg_zero.spawn.clipping_range[1] + ) + def test_camera_resolution_all_colorize(self): """Test camera resolution is correctly set for all types with colorization enabled.""" # Add all types @@ -705,7 +836,7 @@ def test_throughput(self): with Timer(f"Time taken for writing data with shape {camera.image_shape} "): # Pack data back into replicator format to save them using its writer rep_output = {"annotators": {}} - camera_data = convert_dict_to_backend(camera.data.output[0].to_dict(), backend="numpy") + camera_data = convert_dict_to_backend({k: v[0] for k, v in camera.data.output.items()}, backend="numpy") for key, data, info in zip(camera_data.keys(), camera_data.values(), camera.data.info[0].values()): if info is not None: rep_output["annotators"][key] = {"render_product": {"data": data, **info}} @@ -719,6 +850,15 @@ def test_throughput(self): for im_data in camera.data.output.values(): self.assertEqual(im_data.shape, (1, camera_cfg.height, camera_cfg.width, 1)) + def test_sensor_print(self): + """Test sensor print is working correctly.""" + # Create sensor + sensor = Camera(cfg=self.camera_cfg) + # Play sim + self.sim.reset() + # print info + print(sensor) + """ Helper functions. """ diff --git a/source/extensions/omni.isaac.lab/test/sensors/test_contact_sensor.py b/source/extensions/omni.isaac.lab/test/sensors/test_contact_sensor.py index 8390ee2269..6bba29b858 100644 --- a/source/extensions/omni.isaac.lab/test/sensors/test_contact_sensor.py +++ b/source/extensions/omni.isaac.lab/test/sensors/test_contact_sensor.py @@ -294,6 +294,27 @@ def test_cube_stack_contact_filtering(self): contact_sensor_2.data.force_matrix_w[:, :, 0], contact_sensor.data.force_matrix_w[:, :, 0] ) + def test_sensor_print(self): + """Test sensor print is working correctly.""" + with build_simulation_context(device="cuda:0", dt=self.sim_dt, add_lighting=False) as sim: + # Spawn things into stage + scene_cfg = ContactSensorSceneCfg(num_envs=1, env_spacing=1.0, lazy_sensor_update=False) + scene_cfg.terrain = FLAT_TERRAIN_CFG.replace(prim_path="/World/ground") + scene_cfg.shape = CUBE_CFG + scene_cfg.contact_sensor = ContactSensorCfg( + prim_path=scene_cfg.shape.prim_path, + track_pose=True, + debug_vis=False, + update_period=0.0, + track_air_time=True, + history_length=3, + ) + scene = InteractiveScene(scene_cfg) + # Play the simulator + sim.reset() + # print info + print(scene.sensors["contact_sensor"]) + """ Internal helpers. """ diff --git a/source/extensions/omni.isaac.lab/test/sensors/test_frame_transformer.py b/source/extensions/omni.isaac.lab/test/sensors/test_frame_transformer.py index aeab3d9e72..34321e76da 100644 --- a/source/extensions/omni.isaac.lab/test/sensors/test_frame_transformer.py +++ b/source/extensions/omni.isaac.lab/test/sensors/test_frame_transformer.py @@ -579,6 +579,25 @@ def test_frame_transformer_all_bodies(self): torch.testing.assert_close(bodies_pos_source_tf[:, index], body_pos_b) torch.testing.assert_close(bodies_quat_source_tf[:, index], body_quat_b) + def test_sensor_print(self): + """Test sensor print is working correctly.""" + # Spawn things into stage + scene_cfg = MySceneCfg(num_envs=2, env_spacing=5.0, lazy_sensor_update=False) + scene_cfg.frame_transformer = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/base", + target_frames=[ + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/.*", + ), + ], + ) + scene = InteractiveScene(scene_cfg) + + # Play the simulator + self.sim.reset() + # print info + print(scene.sensors["frame_transformer"]) + if __name__ == "__main__": run_tests() diff --git a/source/extensions/omni.isaac.lab/test/sensors/test_imu.py b/source/extensions/omni.isaac.lab/test/sensors/test_imu.py new file mode 100644 index 0000000000..e04211d1a9 --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/sensors/test_imu.py @@ -0,0 +1,538 @@ +# 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 +app_launcher = AppLauncher(headless=True, enable_cameras=True) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import pathlib +import torch +import unittest + +import omni.isaac.core.utils.stage as stage_utils + +import omni.isaac.lab.sim as sim_utils +import omni.isaac.lab.utils.math as math_utils +from omni.isaac.lab.actuators import ImplicitActuatorCfg +from omni.isaac.lab.assets import ArticulationCfg, RigidObjectCfg +from omni.isaac.lab.markers.config import GREEN_ARROW_X_MARKER_CFG, RED_ARROW_X_MARKER_CFG +from omni.isaac.lab.scene import InteractiveScene, InteractiveSceneCfg +from omni.isaac.lab.sensors.imu import ImuCfg +from omni.isaac.lab.terrains import TerrainImporterCfg +from omni.isaac.lab.utils import configclass + +## +# Pre-defined configs +## +from omni.isaac.lab_assets.anymal import ANYMAL_C_CFG # isort: skip +from omni.isaac.lab.utils.assets import NUCLEUS_ASSET_ROOT_DIR # isort: skip + +# offset of imu_link from base_link on anymal_c +POS_OFFSET = (0.2488, 0.00835, 0.04628) +ROT_OFFSET = (0.7071068, 0, 0, 0.7071068) + +# offset of imu_link from link_1 on simple_2_link +PEND_POS_OFFSET = (0.4, 0.0, 0.1) +PEND_ROT_OFFSET = (0.5, 0.5, 0.5, 0.5) + + +@configclass +class MySceneCfg(InteractiveSceneCfg): + """Example scene configuration.""" + + # terrain - flat terrain plane + terrain = TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="plane", + max_init_terrain_level=None, + ) + + # rigid objects - balls + balls = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/ball", + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.5)), + spawn=sim_utils.SphereCfg( + radius=0.25, + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + collision_props=sim_utils.CollisionPropertiesCfg(), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0)), + ), + ) + + cube = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/cube", + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -2.0, 0.5)), + spawn=sim_utils.CuboidCfg( + size=(0.25, 0.25, 0.25), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + collision_props=sim_utils.CollisionPropertiesCfg(), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0)), + ), + ) + + # articulations - robot + robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/robot") + pendulum = ArticulationCfg( + prim_path="{ENV_REGEX_NS}/pendulum", + spawn=sim_utils.UrdfFileCfg( + fix_base=True, + merge_fixed_joints=False, + make_instanceable=False, + asset_path=f"{pathlib.Path(__file__).parent.resolve()}/urdfs/simple_2_link.urdf", + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=True, solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + ), + init_state=ArticulationCfg.InitialStateCfg(), + actuators={ + "joint_1_act": ImplicitActuatorCfg(joint_names_expr=["joint_.*"], stiffness=0.0, damping=0.3), + }, + ) + # sensors - imu (filled inside unit test) + imu_ball: ImuCfg = ImuCfg( + prim_path="{ENV_REGEX_NS}/ball", + gravity_bias=(0.0, 0.0, 0.0), + ) + imu_cube: ImuCfg = ImuCfg( + prim_path="{ENV_REGEX_NS}/cube", + gravity_bias=(0.0, 0.0, 0.0), + ) + imu_robot_imu_link: ImuCfg = ImuCfg( + prim_path="{ENV_REGEX_NS}/robot/imu_link", + gravity_bias=(0.0, 0.0, 0.0), + ) + imu_robot_base: ImuCfg = ImuCfg( + prim_path="{ENV_REGEX_NS}/robot/base", + offset=ImuCfg.OffsetCfg( + pos=POS_OFFSET, + rot=ROT_OFFSET, + ), + gravity_bias=(0.0, 0.0, 0.0), + ) + + imu_pendulum_imu_link: ImuCfg = ImuCfg( + prim_path="{ENV_REGEX_NS}/pendulum/imu_link", + debug_vis=not app_launcher._headless, + visualizer_cfg=RED_ARROW_X_MARKER_CFG.replace(prim_path="/Visuals/Acceleration/imu_link"), + gravity_bias=(0.0, 0.0, 9.81), + ) + imu_pendulum_base: ImuCfg = ImuCfg( + prim_path="{ENV_REGEX_NS}/pendulum/link_1", + offset=ImuCfg.OffsetCfg( + pos=PEND_POS_OFFSET, + rot=PEND_ROT_OFFSET, + ), + debug_vis=not app_launcher._headless, + visualizer_cfg=GREEN_ARROW_X_MARKER_CFG.replace(prim_path="/Visuals/Acceleration/base"), + gravity_bias=(0.0, 0.0, 9.81), + ) + + def __post_init__(self): + """Post initialization.""" + # change position of the robot + self.robot.init_state.pos = (0.0, 2.0, 1.0) + self.pendulum.init_state.pos = (-1.0, 1.0, 0.5) + + # change asset + self.robot.spawn.usd_path = f"{NUCLEUS_ASSET_ROOT_DIR}/Isaac/Robots/ANYbotics/anymal_c.usd" + # change iterations + self.robot.spawn.articulation_props.solver_position_iteration_count = 32 + self.robot.spawn.articulation_props.solver_velocity_iteration_count = 32 + + +class TestImu(unittest.TestCase): + """Test for Imu sensor.""" + + def setUp(self): + """Create a blank new stage for each test.""" + # Create a new stage + stage_utils.create_new_stage() + # Load simulation context + sim_cfg = sim_utils.SimulationCfg(dt=0.001) + sim_cfg.physx.solver_type = 0 # 0: PGS, 1: TGS --> use PGS for more accurate results + self.sim = sim_utils.SimulationContext(sim_cfg) + # construct scene + scene_cfg = MySceneCfg(num_envs=2, env_spacing=5.0, lazy_sensor_update=False) + self.scene = InteractiveScene(scene_cfg) + # Play the simulator + self.sim.reset() + + def tearDown(self): + """Stops simulator after each test.""" + # clear the stage + self.sim.clear_all_callbacks() + self.sim.clear_instance() + + """ + Tests + """ + + def test_constant_velocity(self): + """Test the Imu sensor with a constant velocity. + + Expected behavior is that the linear and angular are approx the same at every time step as in each step we set + the same velocity and therefore reset the physx buffers.""" + prev_lin_acc_ball = torch.zeros((self.scene.num_envs, 3), dtype=torch.float32, device=self.scene.device) + prev_ang_acc_ball = torch.zeros((self.scene.num_envs, 3), dtype=torch.float32, device=self.scene.device) + prev_lin_acc_cube = torch.zeros((self.scene.num_envs, 3), dtype=torch.float32, device=self.scene.device) + prev_ang_acc_cube = torch.zeros((self.scene.num_envs, 3), dtype=torch.float32, device=self.scene.device) + + for idx in range(200): + # set velocity + self.scene.rigid_objects["balls"].write_root_velocity_to_sim( + torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]], dtype=torch.float32, device=self.scene.device).repeat( + self.scene.num_envs, 1 + ) + ) + self.scene.rigid_objects["cube"].write_root_velocity_to_sim( + torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]], dtype=torch.float32, device=self.scene.device).repeat( + self.scene.num_envs, 1 + ) + ) + # write data to sim + self.scene.write_data_to_sim() + + # perform step + self.sim.step() + # read data from sim + self.scene.update(self.sim.get_physics_dt()) + + if idx > 1: + # check the imu accelerations + torch.testing.assert_close( + self.scene.sensors["imu_ball"].data.lin_acc_b, + prev_lin_acc_ball, + rtol=1e-3, + atol=1e-3, + ) + torch.testing.assert_close( + self.scene.sensors["imu_ball"].data.ang_acc_b, + prev_ang_acc_ball, + rtol=1e-3, + atol=1e-3, + ) + + torch.testing.assert_close( + self.scene.sensors["imu_cube"].data.lin_acc_b, + prev_lin_acc_cube, + rtol=1e-3, + atol=1e-3, + ) + torch.testing.assert_close( + self.scene.sensors["imu_cube"].data.ang_acc_b, + prev_ang_acc_cube, + rtol=1e-3, + atol=1e-3, + ) + + # check the imu velocities + # NOTE: the expected lin_vel_b is the same as the set velocity, as write_root_velocity_to_sim is + # setting v_0 (initial velocity) and then a calculation step of v_i = v_0 + a*dt. Consequently, + # the data.lin_vel_b is returning approx. v_i. + torch.testing.assert_close( + self.scene.sensors["imu_ball"].data.lin_vel_b, + torch.tensor( + [[1.0, 0.0, -self.scene.physics_dt * 9.81]], dtype=torch.float32, device=self.scene.device + ).repeat(self.scene.num_envs, 1), + rtol=1e-4, + atol=1e-4, + ) + torch.testing.assert_close( + self.scene.sensors["imu_cube"].data.lin_vel_b, + torch.tensor( + [[1.0, 0.0, -self.scene.physics_dt * 9.81]], dtype=torch.float32, device=self.scene.device + ).repeat(self.scene.num_envs, 1), + rtol=1e-4, + atol=1e-4, + ) + + # update previous values + prev_lin_acc_ball = self.scene.sensors["imu_ball"].data.lin_acc_b.clone() + prev_ang_acc_ball = self.scene.sensors["imu_ball"].data.ang_acc_b.clone() + prev_lin_acc_cube = self.scene.sensors["imu_cube"].data.lin_acc_b.clone() + prev_ang_acc_cube = self.scene.sensors["imu_cube"].data.ang_acc_b.clone() + + def test_constant_acceleration(self): + """Test the Imu sensor with a constant acceleration.""" + for idx in range(100): + # set acceleration + self.scene.rigid_objects["balls"].write_root_velocity_to_sim( + torch.tensor([[0.1, 0.0, 0.0, 0.0, 0.0, 0.0]], dtype=torch.float32, device=self.scene.device).repeat( + self.scene.num_envs, 1 + ) + * (idx + 1) + ) + # write data to sim + self.scene.write_data_to_sim() + # perform step + self.sim.step() + # read data from sim + self.scene.update(self.sim.get_physics_dt()) + + # skip first step where initial velocity is zero + if idx < 1: + continue + + # check the imu data + torch.testing.assert_close( + self.scene.sensors["imu_ball"].data.lin_acc_b, + math_utils.quat_rotate_inverse( + self.scene.rigid_objects["balls"].data.root_quat_w, + torch.tensor([[0.1, 0.0, 0.0]], dtype=torch.float32, device=self.scene.device).repeat( + self.scene.num_envs, 1 + ) + / self.sim.get_physics_dt(), + ), + rtol=1e-4, + atol=1e-4, + ) + + # check the angular velocity + torch.testing.assert_close( + self.scene.sensors["imu_ball"].data.ang_vel_b, + self.scene.rigid_objects["balls"].data.root_ang_vel_b, + rtol=1e-4, + atol=1e-4, + ) + + def test_single_dof_pendulum(self): + """Test imu against analytical pendulum problem.""" + + # pendulum length + pend_length = PEND_POS_OFFSET[0] + + # should achieve same results between the two imu sensors on the robot + for idx in range(500): + + # write data to sim + self.scene.write_data_to_sim() + # perform step + self.sim.step() + # read data from sim + self.scene.update(self.sim.get_physics_dt()) + + # get pendulum joint state + joint_pos = self.scene.articulations["pendulum"].data.joint_pos + joint_vel = self.scene.articulations["pendulum"].data.joint_vel + joint_acc = self.scene.articulations["pendulum"].data.joint_acc + + # IMU and base data + imu_data = self.scene.sensors["imu_pendulum_imu_link"].data + base_data = self.scene.sensors["imu_pendulum_base"].data + + # extract imu_link imu_sensor dynamics + lin_vel_w_imu_link = math_utils.quat_rotate(imu_data.quat_w, imu_data.lin_vel_b) + lin_acc_w_imu_link = math_utils.quat_rotate(imu_data.quat_w, imu_data.lin_acc_b) + + # calculate the joint dynamics from the imu_sensor (y axis of imu_link is parallel to joint axis of pendulum) + joint_vel_imu = math_utils.quat_rotate(imu_data.quat_w, imu_data.ang_vel_b)[..., 1].unsqueeze(-1) + joint_acc_imu = math_utils.quat_rotate(imu_data.quat_w, imu_data.ang_acc_b)[..., 1].unsqueeze(-1) + + # calculate analytical solution + vx = -joint_vel * pend_length * torch.sin(joint_pos) + vy = torch.zeros(2, 1, device=self.scene.device) + vz = -joint_vel * pend_length * torch.cos(joint_pos) + gt_linear_vel_w = torch.cat([vx, vy, vz], dim=-1) + + ax = -joint_acc * pend_length * torch.sin(joint_pos) - joint_vel**2 * pend_length * torch.cos(joint_pos) + ay = torch.zeros(2, 1, device=self.scene.device) + az = ( + -joint_acc * pend_length * torch.cos(joint_pos) + + joint_vel**2 * pend_length * torch.sin(joint_pos) + + 9.81 + ) + gt_linear_acc_w = torch.cat([ax, ay, az], dim=-1) + + # skip first step where initial velocity is zero + if idx < 2: + continue + + # compare imu angular velocity with joint velocity + torch.testing.assert_close( + joint_vel, + joint_vel_imu, + rtol=1e-1, + atol=1e-3, + ) + # compare imu angular acceleration with joint acceleration + torch.testing.assert_close( + joint_acc, + joint_acc_imu, + rtol=1e-1, + atol=1e-3, + ) + # compare imu linear velocituy with simple pendulum calculation + torch.testing.assert_close( + gt_linear_vel_w, + lin_vel_w_imu_link, + rtol=1e-1, + atol=1e-3, + ) + # compare imu linear acceleration with simple pendulum calculation + torch.testing.assert_close( + gt_linear_acc_w, + lin_acc_w_imu_link, + rtol=1e-1, + atol=1e0, + ) + + # check the position between offset and imu definition + torch.testing.assert_close( + base_data.pos_w, + imu_data.pos_w, + rtol=1e-5, + atol=1e-5, + ) + + # check the orientation between offset and imu definition + torch.testing.assert_close( + base_data.quat_w, + imu_data.quat_w, + rtol=1e-4, + atol=1e-4, + ) + + # check the angular velocities of the imus between offset and imu definition + torch.testing.assert_close( + base_data.ang_vel_b, + imu_data.ang_vel_b, + rtol=1e-4, + atol=1e-4, + ) + # check the angular acceleration of the imus between offset and imu definition + torch.testing.assert_close( + base_data.ang_acc_b, + imu_data.ang_acc_b, + rtol=1e-4, + atol=1e-4, + ) + + # check the linear velocity of the imus between offset and imu definition + torch.testing.assert_close( + base_data.lin_vel_b, + imu_data.lin_vel_b, + rtol=1e-2, + atol=5e-3, + ) + + # check the linear acceleration of the imus between offset and imu definition + torch.testing.assert_close( + base_data.lin_acc_b, + imu_data.lin_acc_b, + rtol=1e-1, + atol=1e-1, + ) + + def test_offset_calculation(self): + """Test offset configuration argument.""" + # should achieve same results between the two imu sensors on the robot + for idx in range(500): + # set acceleration + self.scene.articulations["robot"].write_root_velocity_to_sim( + torch.tensor([[0.05, 0.0, 0.0, 0.0, 0.0, 0.0]], dtype=torch.float32, device=self.scene.device).repeat( + self.scene.num_envs, 1 + ) + * (idx + 1) + ) + # write data to sim + self.scene.write_data_to_sim() + # perform step + self.sim.step() + # read data from sim + self.scene.update(self.sim.get_physics_dt()) + + # skip first step where initial velocity is zero + if idx < 1: + continue + + # check the accelerations + torch.testing.assert_close( + self.scene.sensors["imu_robot_base"].data.lin_acc_b, + self.scene.sensors["imu_robot_imu_link"].data.lin_acc_b, + rtol=1e-4, + atol=1e-4, + ) + torch.testing.assert_close( + self.scene.sensors["imu_robot_base"].data.ang_acc_b, + self.scene.sensors["imu_robot_imu_link"].data.ang_acc_b, + rtol=1e-4, + atol=1e-4, + ) + + # check the velocities + torch.testing.assert_close( + self.scene.sensors["imu_robot_base"].data.ang_vel_b, + self.scene.sensors["imu_robot_imu_link"].data.ang_vel_b, + rtol=1e-4, + atol=1e-4, + ) + torch.testing.assert_close( + self.scene.sensors["imu_robot_base"].data.lin_vel_b, + self.scene.sensors["imu_robot_imu_link"].data.lin_vel_b, + rtol=1e-4, + atol=1e-4, + ) + + # check the orientation + torch.testing.assert_close( + self.scene.sensors["imu_robot_base"].data.quat_w, + self.scene.sensors["imu_robot_imu_link"].data.quat_w, + rtol=1e-4, + atol=1e-4, + ) + # check the position + torch.testing.assert_close( + self.scene.sensors["imu_robot_base"].data.pos_w, + self.scene.sensors["imu_robot_imu_link"].data.pos_w, + rtol=1e-4, + atol=1e-4, + ) + + def test_env_ids_propogation(self): + """Test that env_ids argument propagates through update and reset methods""" + self.scene.reset() + + for idx in range(10): + # set acceleration + self.scene.articulations["robot"].write_root_velocity_to_sim( + torch.tensor([[0.5, 0.0, 0.0, 0.0, 0.0, 0.0]], dtype=torch.float32, device=self.scene.device).repeat( + self.scene.num_envs, 1 + ) + * (idx + 1) + ) + # write data to sim + self.scene.write_data_to_sim() + # perform step + self.sim.step() + # read data from sim + self.scene.update(self.sim.get_physics_dt()) + + # reset scene for env 1 + self.scene.reset(env_ids=[1]) + # read data from sim + self.scene.update(self.sim.get_physics_dt()) + # perform step + self.sim.step() + # read data from sim + self.scene.update(self.sim.get_physics_dt()) + + def test_sensor_print(self): + """Test sensor print is working correctly.""" + # Create sensor + sensor = self.scene.sensors["imu_ball"] + # print info + print(sensor) + + +if __name__ == "__main__": + run_tests() diff --git a/source/extensions/omni.isaac.lab/test/sensors/test_ray_caster_camera.py b/source/extensions/omni.isaac.lab/test/sensors/test_ray_caster_camera.py index b28d023c85..e369eb569d 100644 --- a/source/extensions/omni.isaac.lab/test/sensors/test_ray_caster_camera.py +++ b/source/extensions/omni.isaac.lab/test/sensors/test_ray_caster_camera.py @@ -129,7 +129,7 @@ def test_camera_init(self): # update camera camera.update(self.dt) # check image data - for im_data in camera.data.output.to_dict().values(): + for im_data in camera.data.output.values(): self.assertEqual( im_data.shape, (1, self.camera_cfg.pattern_cfg.height, self.camera_cfg.pattern_cfg.width, 1) ) @@ -147,11 +147,112 @@ def test_camera_resolution(self): self.sim.step() camera.update(self.dt) # access image data and compare shapes - for im_data in camera.data.output.to_dict().values(): + for im_data in camera.data.output.values(): self.assertTrue( im_data.shape == (1, self.camera_cfg.pattern_cfg.height, self.camera_cfg.pattern_cfg.width, 1) ) + def test_depth_clipping(self): + """Test depth clipping. + + .. note:: + + This test is the same for all camera models to enforce the same clipping behavior. + """ + prim_utils.create_prim("/World/CameraZero", "Xform") + prim_utils.create_prim("/World/CameraNone", "Xform") + prim_utils.create_prim("/World/CameraMax", "Xform") + + # get camera cfgs + camera_cfg_zero = RayCasterCameraCfg( + prim_path="/World/CameraZero", + mesh_prim_paths=["/World/defaultGroundPlane"], + offset=RayCasterCameraCfg.OffsetCfg( + pos=(2.5, 2.5, 6.0), rot=(0.9914449, 0.0, 0.1305, 0.0), convention="world" + ), + pattern_cfg=patterns.PinholeCameraPatternCfg().from_intrinsic_matrix( + focal_length=38.0, + intrinsic_matrix=[380.08, 0.0, 467.79, 0.0, 380.08, 262.05, 0.0, 0.0, 1.0], + height=540, + width=960, + ), + max_distance=10.0, + data_types=["distance_to_image_plane", "distance_to_camera"], + depth_clipping_behavior="zero", + ) + camera_zero = RayCasterCamera(camera_cfg_zero) + + camera_cfg_none = copy.deepcopy(camera_cfg_zero) + camera_cfg_none.prim_path = "/World/CameraNone" + camera_cfg_none.depth_clipping_behavior = "none" + camera_none = RayCasterCamera(camera_cfg_none) + + camera_cfg_max = copy.deepcopy(camera_cfg_zero) + camera_cfg_max.prim_path = "/World/CameraMax" + camera_cfg_max.depth_clipping_behavior = "max" + camera_max = RayCasterCamera(camera_cfg_max) + + # Play sim + self.sim.reset() + + camera_zero.update(self.dt) + camera_none.update(self.dt) + camera_max.update(self.dt) + + # none clipping should contain inf values + self.assertTrue(torch.isinf(camera_none.data.output["distance_to_camera"]).any()) + self.assertTrue(torch.isnan(camera_none.data.output["distance_to_image_plane"]).any()) + self.assertTrue( + camera_none.data.output["distance_to_camera"][ + ~torch.isinf(camera_none.data.output["distance_to_camera"]) + ].max() + > camera_cfg_zero.max_distance + ) + self.assertTrue( + camera_none.data.output["distance_to_image_plane"][ + ~torch.isnan(camera_none.data.output["distance_to_image_plane"]) + ].max() + > camera_cfg_zero.max_distance + ) + + # zero clipping should result in zero values + self.assertTrue( + torch.all( + camera_zero.data.output["distance_to_camera"][ + torch.isinf(camera_none.data.output["distance_to_camera"]) + ] + == 0.0 + ) + ) + self.assertTrue( + torch.all( + camera_zero.data.output["distance_to_image_plane"][ + torch.isnan(camera_none.data.output["distance_to_image_plane"]) + ] + == 0.0 + ) + ) + self.assertTrue(camera_zero.data.output["distance_to_camera"].max() <= camera_cfg_zero.max_distance) + self.assertTrue(camera_zero.data.output["distance_to_image_plane"].max() <= camera_cfg_zero.max_distance) + + # max clipping should result in max values + self.assertTrue( + torch.all( + camera_max.data.output["distance_to_camera"][torch.isinf(camera_none.data.output["distance_to_camera"])] + == camera_cfg_zero.max_distance + ) + ) + self.assertTrue( + torch.all( + camera_max.data.output["distance_to_image_plane"][ + torch.isnan(camera_none.data.output["distance_to_image_plane"]) + ] + == camera_cfg_zero.max_distance + ) + ) + self.assertTrue(camera_max.data.output["distance_to_camera"].max() <= camera_cfg_zero.max_distance) + self.assertTrue(camera_max.data.output["distance_to_image_plane"].max() <= camera_cfg_zero.max_distance) + def test_camera_init_offset(self): """Test camera initialization with offset using different conventions.""" # define the same offset in all conventions @@ -289,7 +390,7 @@ def test_multi_camera_init(self): cam_2.update(self.dt) # check image data for cam in [cam_1, cam_2]: - for im_data in cam.data.output.to_dict().values(): + for im_data in cam.data.output.values(): self.assertEqual( im_data.shape, (1, self.camera_cfg.pattern_cfg.height, self.camera_cfg.pattern_cfg.width, 1) ) @@ -392,7 +493,7 @@ def test_throughput(self): with Timer(f"Time taken for writing data with shape {camera.image_shape} "): # Pack data back into replicator format to save them using its writer rep_output = {"annotators": {}} - camera_data = convert_dict_to_backend(camera.data.output[0].to_dict(), backend="numpy") + camera_data = convert_dict_to_backend({k: v[0] for k, v in camera.data.output.items()}, backend="numpy") for key, data, info in zip(camera_data.keys(), camera_data.values(), camera.data.info[0].values()): if info is not None: rep_output["annotators"][key] = {"render_product": {"data": data, **info}} @@ -817,6 +918,15 @@ def test_output_equal_to_usd_camera_when_intrinsics_set(self): atol=1e-4, ) + def test_sensor_print(self): + """Test sensor print is working correctly.""" + # Create sensor + sensor = RayCasterCamera(cfg=self.camera_cfg) + # Play sim + self.sim.reset() + # print info + print(sensor) + if __name__ == "__main__": run_tests() diff --git a/source/extensions/omni.isaac.lab/test/sensors/test_tiled_camera.py b/source/extensions/omni.isaac.lab/test/sensors/test_tiled_camera.py index 269cb4bd81..38b79718b7 100644 --- a/source/extensions/omni.isaac.lab/test/sensors/test_tiled_camera.py +++ b/source/extensions/omni.isaac.lab/test/sensors/test_tiled_camera.py @@ -111,7 +111,7 @@ def test_single_camera_init(self): # update camera camera.update(self.dt) # check image data - for im_type, im_data in camera.data.output.to_dict().items(): + for im_type, im_data in camera.data.output.items(): if im_type == "rgb": self.assertEqual(im_data.shape, (1, self.camera_cfg.height, self.camera_cfg.width, 3)) self.assertGreater((im_data / 255.0).mean().item(), 0.0) @@ -120,6 +120,117 @@ def test_single_camera_init(self): self.assertGreater(im_data.mean().item(), 0.0) del camera + def test_depth_clipping_max(self): + """Test depth max clipping.""" + # get camera cfgs + camera_cfg = TiledCameraCfg( + prim_path="/World/Camera", + offset=TiledCameraCfg.OffsetCfg(pos=(2.5, 2.5, 6.0), rot=(-0.125, 0.362, 0.873, -0.302), convention="ros"), + spawn=sim_utils.PinholeCameraCfg().from_intrinsic_matrix( + focal_length=38.0, + intrinsic_matrix=[380.08, 0.0, 467.79, 0.0, 380.08, 262.05, 0.0, 0.0, 1.0], + height=540, + width=960, + clipping_range=(4.9, 5.0), + ), + height=540, + width=960, + data_types=["depth"], + depth_clipping_behavior="max", + ) + camera = TiledCamera(camera_cfg) + + # Play sim + self.sim.reset() + + # note: This is a workaround to ensure that the textures are loaded. + # Check "Known Issues" section in the documentation for more details. + for _ in range(5): + self.sim.step() + + camera.update(self.dt) + + self.assertTrue(len(camera.data.output["depth"][torch.isinf(camera.data.output["depth"])]) == 0) + self.assertTrue(camera.data.output["depth"].min() >= camera_cfg.spawn.clipping_range[0]) + self.assertTrue(camera.data.output["depth"].max() <= camera_cfg.spawn.clipping_range[1]) + + del camera + + def test_depth_clipping_none(self): + """Test depth none clipping.""" + # get camera cfgs + camera_cfg = TiledCameraCfg( + prim_path="/World/Camera", + offset=TiledCameraCfg.OffsetCfg(pos=(2.5, 2.5, 6.0), rot=(-0.125, 0.362, 0.873, -0.302), convention="ros"), + spawn=sim_utils.PinholeCameraCfg().from_intrinsic_matrix( + focal_length=38.0, + intrinsic_matrix=[380.08, 0.0, 467.79, 0.0, 380.08, 262.05, 0.0, 0.0, 1.0], + height=540, + width=960, + clipping_range=(4.9, 5.0), + ), + height=540, + width=960, + data_types=["depth"], + depth_clipping_behavior="none", + ) + camera = TiledCamera(camera_cfg) + + # Play sim + self.sim.reset() + + # note: This is a workaround to ensure that the textures are loaded. + # Check "Known Issues" section in the documentation for more details. + for _ in range(5): + self.sim.step() + + camera.update(self.dt) + + self.assertTrue(len(camera.data.output["depth"][torch.isinf(camera.data.output["depth"])]) > 0) + self.assertTrue(camera.data.output["depth"].min() >= camera_cfg.spawn.clipping_range[0]) + self.assertTrue( + camera.data.output["depth"][~torch.isinf(camera.data.output["depth"])].max() + <= camera_cfg.spawn.clipping_range[1] + ) + + del camera + + def test_depth_clipping_zero(self): + """Test depth zero clipping.""" + # get camera cfgs + camera_cfg = TiledCameraCfg( + prim_path="/World/Camera", + offset=TiledCameraCfg.OffsetCfg(pos=(2.5, 2.5, 6.0), rot=(-0.125, 0.362, 0.873, -0.302), convention="ros"), + spawn=sim_utils.PinholeCameraCfg().from_intrinsic_matrix( + focal_length=38.0, + intrinsic_matrix=[380.08, 0.0, 467.79, 0.0, 380.08, 262.05, 0.0, 0.0, 1.0], + height=540, + width=960, + clipping_range=(4.9, 5.0), + ), + height=540, + width=960, + data_types=["depth"], + depth_clipping_behavior="zero", + ) + camera = TiledCamera(camera_cfg) + + # Play sim + self.sim.reset() + + # note: This is a workaround to ensure that the textures are loaded. + # Check "Known Issues" section in the documentation for more details. + for _ in range(5): + self.sim.step() + + camera.update(self.dt) + + self.assertTrue(len(camera.data.output["depth"][torch.isinf(camera.data.output["depth"])]) == 0) + self.assertTrue(camera.data.output["depth"].min() == 0.0) + self.assertTrue(camera.data.output["depth"].max() <= camera_cfg.spawn.clipping_range[1]) + + del camera + def test_multi_camera_init(self): """Test multi-camera initialization.""" @@ -162,7 +273,7 @@ def test_multi_camera_init(self): # update camera camera.update(self.dt) # check image data - for im_type, im_data in camera.data.output.to_dict().items(): + for im_type, im_data in camera.data.output.items(): if im_type == "rgb": self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 3)) for i in range(4): @@ -347,7 +458,7 @@ def test_rgba_only_camera(self): # update camera camera.update(self.dt) # check image data - for _, im_data in camera.data.output.to_dict().items(): + for _, im_data in camera.data.output.items(): self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 4)) for i in range(4): self.assertGreater((im_data[i] / 255.0).mean().item(), 0.0) @@ -399,7 +510,7 @@ def test_distance_to_camera_only_camera(self): # update camera camera.update(self.dt) # check image data - for _, im_data in camera.data.output.to_dict().items(): + for _, im_data in camera.data.output.items(): self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 1)) for i in range(4): self.assertGreater((im_data[i]).mean().item(), 0.0) @@ -451,7 +562,7 @@ def test_distance_to_image_plane_only_camera(self): # update camera camera.update(self.dt) # check image data - for _, im_data in camera.data.output.to_dict().items(): + for _, im_data in camera.data.output.items(): self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 1)) for i in range(4): self.assertGreater((im_data[i]).mean().item(), 0.0) @@ -503,7 +614,7 @@ def test_normals_only_camera(self): # update camera camera.update(self.dt) # check image data - for _, im_data in camera.data.output.to_dict().items(): + for _, im_data in camera.data.output.items(): self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 3)) for i in range(4): self.assertGreater((im_data[i]).mean().item(), 0.0) @@ -555,7 +666,7 @@ def test_motion_vectors_only_camera(self): # update camera camera.update(self.dt) # check image data - for _, im_data in camera.data.output.to_dict().items(): + for _, im_data in camera.data.output.items(): self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 2)) for i in range(4): self.assertGreater((im_data[i]).mean().item(), 0.0) @@ -607,7 +718,7 @@ def test_semantic_segmentation_colorize_only_camera(self): # update camera camera.update(self.dt) # check image data - for _, im_data in camera.data.output.to_dict().items(): + for _, im_data in camera.data.output.items(): self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 4)) for i in range(4): self.assertGreater((im_data[i] / 255.0).mean().item(), 0.0) @@ -660,7 +771,7 @@ def test_instance_segmentation_fast_colorize_only_camera(self): # update camera camera.update(self.dt) # check image data - for _, im_data in camera.data.output.to_dict().items(): + for _, im_data in camera.data.output.items(): self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 4)) for i in range(num_cameras): self.assertGreater((im_data[i] / 255.0).mean().item(), 0.0) @@ -713,7 +824,7 @@ def test_instance_id_segmentation_fast_colorize_only_camera(self): # update camera camera.update(self.dt) # check image data - for _, im_data in camera.data.output.to_dict().items(): + for _, im_data in camera.data.output.items(): self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 4)) for i in range(num_cameras): self.assertGreater((im_data[i] / 255.0).mean().item(), 0.0) @@ -767,7 +878,7 @@ def test_semantic_segmentation_non_colorize_only_camera(self): # update camera camera.update(self.dt) # check image data - for _, im_data in camera.data.output.to_dict().items(): + for _, im_data in camera.data.output.items(): self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 1)) for i in range(num_cameras): self.assertGreater(im_data[i].to(dtype=float).mean().item(), 0.0) @@ -822,7 +933,7 @@ def test_instance_segmentation_fast_non_colorize_only_camera(self): # update camera camera.update(self.dt) # check image data - for _, im_data in camera.data.output.to_dict().items(): + for _, im_data in camera.data.output.items(): self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 1)) for i in range(num_cameras): self.assertGreater(im_data[i].to(dtype=float).mean().item(), 0.0) @@ -876,7 +987,7 @@ def test_instance_id_segmentation_fast_non_colorize_only_camera(self): # update camera camera.update(self.dt) # check image data - for _, im_data in camera.data.output.to_dict().items(): + for _, im_data in camera.data.output.items(): self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 1)) for i in range(num_cameras): self.assertGreater(im_data[i].to(dtype=float).mean().item(), 0.0) @@ -941,7 +1052,7 @@ def test_all_annotators_camera(self): # update camera camera.update(self.dt) # check image data - for data_type, im_data in camera.data.output.to_dict().items(): + for data_type, im_data in camera.data.output.items(): if data_type in ["rgb", "normals"]: self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 3)) elif data_type in [ @@ -1039,7 +1150,7 @@ def test_all_annotators_low_resolution_camera(self): # update camera camera.update(self.dt) # check image data - for data_type, im_data in camera.data.output.to_dict().items(): + for data_type, im_data in camera.data.output.items(): if data_type in ["rgb", "normals"]: self.assertEqual(im_data.shape, (num_cameras, camera_cfg.height, camera_cfg.width, 3)) elif data_type in [ @@ -1135,7 +1246,7 @@ def test_all_annotators_non_perfect_square_number_camera(self): # update camera camera.update(self.dt) # check image data - for data_type, im_data in camera.data.output.to_dict().items(): + for data_type, im_data in camera.data.output.items(): if data_type in ["rgb", "normals"]: self.assertEqual(im_data.shape, (num_cameras, self.camera_cfg.height, self.camera_cfg.width, 3)) elif data_type in [ @@ -1201,7 +1312,7 @@ def test_throughput(self): with Timer(f"Time taken for updating camera with shape {camera.image_shape}"): camera.update(self.dt) # Check image data - for im_type, im_data in camera.data.output.to_dict().items(): + for im_type, im_data in camera.data.output.items(): if im_type == "rgb": self.assertEqual(im_data.shape, (1, camera_cfg.height, camera_cfg.width, 3)) self.assertGreater((im_data / 255.0).mean().item(), 0.0) @@ -1304,6 +1415,15 @@ def test_output_equal_to_usd_camera_intrinsics(self): del camera_tiled del camera_usd + def test_sensor_print(self): + """Test sensor print is working correctly.""" + # Create sensor + sensor = TiledCamera(cfg=self.camera_cfg) + # Play sim + self.sim.reset() + # print info + print(sensor) + """ Helper functions. """ diff --git a/source/extensions/omni.isaac.lab/test/sensors/urdfs/simple_2_link.urdf b/source/extensions/omni.isaac.lab/test/sensors/urdfs/simple_2_link.urdf new file mode 100644 index 0000000000..7c09e1b82c --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/sensors/urdfs/simple_2_link.urdf @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/extensions/omni.isaac.lab/test/sim/test_mesh_converter.py b/source/extensions/omni.isaac.lab/test/sim/test_mesh_converter.py index 4d67db879c..a77e0ad8f0 100644 --- a/source/extensions/omni.isaac.lab/test/sim/test_mesh_converter.py +++ b/source/extensions/omni.isaac.lab/test/sim/test_mesh_converter.py @@ -12,7 +12,9 @@ """Rest everything follows.""" +import math import os +import random import tempfile import unittest @@ -27,6 +29,16 @@ from omni.isaac.lab.utils.assets import ISAACLAB_NUCLEUS_DIR, retrieve_file_path +def random_quaternion(): + # Generate four random numbers for the quaternion + u1, u2, u3 = random.random(), random.random(), random.random() + w = math.sqrt(1 - u1) * math.sin(2 * math.pi * u2) + x = math.sqrt(1 - u1) * math.cos(2 * math.pi * u2) + y = math.sqrt(u1) * math.sin(2 * math.pi * u3) + z = math.sqrt(u1) * math.cos(2 * math.pi * u3) + return (w, x, y, z) + + class TestMeshConverter(unittest.TestCase): """Test fixture for the MeshConverter class.""" @@ -105,7 +117,12 @@ def test_config_change(self): def test_convert_obj(self): """Convert an OBJ file""" - mesh_config = MeshConverterCfg(asset_path=self.assets["obj"]) + mesh_config = MeshConverterCfg( + asset_path=self.assets["obj"], + scale=(random.uniform(0.1, 2.0), random.uniform(0.1, 2.0), random.uniform(0.1, 2.0)), + translation=(random.uniform(-10.0, 10.0), random.uniform(-10.0, 10.0), random.uniform(-10.0, 10.0)), + rotation=random_quaternion(), + ) mesh_converter = MeshConverter(mesh_config) # check that mesh conversion is successful @@ -113,7 +130,12 @@ def test_convert_obj(self): def test_convert_stl(self): """Convert an STL file""" - mesh_config = MeshConverterCfg(asset_path=self.assets["stl"]) + mesh_config = MeshConverterCfg( + asset_path=self.assets["stl"], + scale=(random.uniform(0.1, 2.0), random.uniform(0.1, 2.0), random.uniform(0.1, 2.0)), + translation=(random.uniform(-10.0, 10.0), random.uniform(-10.0, 10.0), random.uniform(-10.0, 10.0)), + rotation=random_quaternion(), + ) mesh_converter = MeshConverter(mesh_config) # check that mesh conversion is successful @@ -121,12 +143,24 @@ def test_convert_stl(self): def test_convert_fbx(self): """Convert an FBX file""" - mesh_config = MeshConverterCfg(asset_path=self.assets["fbx"]) + mesh_config = MeshConverterCfg( + asset_path=self.assets["fbx"], + scale=(random.uniform(0.1, 2.0), random.uniform(0.1, 2.0), random.uniform(0.1, 2.0)), + translation=(random.uniform(-10.0, 10.0), random.uniform(-10.0, 10.0), random.uniform(-10.0, 10.0)), + rotation=random_quaternion(), + ) mesh_converter = MeshConverter(mesh_config) # check that mesh conversion is successful self._check_mesh_conversion(mesh_converter) + def test_convert_default_xform_transforms(self): + """Convert an OBJ file and check that default xform transforms are applied correctly""" + mesh_config = MeshConverterCfg(asset_path=self.assets["obj"]) + mesh_converter = MeshConverter(mesh_config) + # check that mesh conversion is successful + self._check_mesh_conversion(mesh_converter) + def test_collider_no_approximation(self): """Convert an OBJ file using no approximation""" collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True) @@ -229,6 +263,15 @@ def _check_mesh_conversion(self, mesh_converter: MeshConverter): units = UsdGeom.GetStageMetersPerUnit(stage) self.assertEqual(units, 1.0) + # Check mesh settings + pos = tuple(prim_utils.get_prim_at_path("/World/Object/geometry").GetAttribute("xformOp:translate").Get()) + self.assertEqual(pos, mesh_converter.cfg.translation) + quat = prim_utils.get_prim_at_path("/World/Object/geometry").GetAttribute("xformOp:orient").Get() + quat = (quat.GetReal(), quat.GetImaginary()[0], quat.GetImaginary()[1], quat.GetImaginary()[2]) + self.assertEqual(quat, mesh_converter.cfg.rotation) + scale = tuple(prim_utils.get_prim_at_path("/World/Object/geometry").GetAttribute("xformOp:scale").Get()) + self.assertEqual(scale, mesh_converter.cfg.scale) + def _check_mesh_collider_settings(self, mesh_converter: MeshConverter): # Check prim can be properly spawned prim_path = "/World/Object" diff --git a/source/extensions/omni.isaac.lab/test/sim/test_simulation_context.py b/source/extensions/omni.isaac.lab/test/sim/test_simulation_context.py index 4a4f308a8f..7829da9c1c 100644 --- a/source/extensions/omni.isaac.lab/test/sim/test_simulation_context.py +++ b/source/extensions/omni.isaac.lab/test/sim/test_simulation_context.py @@ -12,7 +12,6 @@ """Rest everything follows.""" -import ctypes import numpy as np import unittest @@ -94,33 +93,33 @@ def test_headless_mode(self): # check default render mode self.assertEqual(sim.render_mode, sim.RenderMode.NO_GUI_OR_RENDERING) - def test_boundedness(self): - """Test that the boundedness of the simulation context remains constant. - - Note: This test fails right now because Isaac Sim does not handle boundedness correctly. On creation, - it is registering itself to various callbacks and hence the boundedness is more than 1. This may not be - critical for the simulation context since we usually call various clear functions before deleting the - simulation context. - """ - sim = SimulationContext() - # manually set the boundedness to 1? -- this is not possible because of Isaac Sim. - sim.clear_all_callbacks() - sim._stage_open_callback = None - sim._physics_timer_callback = None - sim._event_timer_callback = None - - # check that boundedness of simulation context is correct - sim_ref_count = ctypes.c_long.from_address(id(sim)).value - # reset the simulation - sim.reset() - self.assertEqual(ctypes.c_long.from_address(id(sim)).value, sim_ref_count) - # step the simulation - for _ in range(10): - sim.step() - self.assertEqual(ctypes.c_long.from_address(id(sim)).value, sim_ref_count) - # clear the simulation - sim.clear_instance() - self.assertEqual(ctypes.c_long.from_address(id(sim)).value, sim_ref_count - 1) + # def test_boundedness(self): + # """Test that the boundedness of the simulation context remains constant. + + # Note: This test fails right now because Isaac Sim does not handle boundedness correctly. On creation, + # it is registering itself to various callbacks and hence the boundedness is more than 1. This may not be + # critical for the simulation context since we usually call various clear functions before deleting the + # simulation context. + # """ + # sim = SimulationContext() + # # manually set the boundedness to 1? -- this is not possible because of Isaac Sim. + # sim.clear_all_callbacks() + # sim._stage_open_callback = None + # sim._physics_timer_callback = None + # sim._event_timer_callback = None + + # # check that boundedness of simulation context is correct + # sim_ref_count = ctypes.c_long.from_address(id(sim)).value + # # reset the simulation + # sim.reset() + # self.assertEqual(ctypes.c_long.from_address(id(sim)).value, sim_ref_count) + # # step the simulation + # for _ in range(10): + # sim.step() + # self.assertEqual(ctypes.c_long.from_address(id(sim)).value, sim_ref_count) + # # clear the simulation + # sim.clear_instance() + # self.assertEqual(ctypes.c_long.from_address(id(sim)).value, sim_ref_count - 1) def test_zero_gravity(self): """Test that gravity can be properly disabled.""" diff --git a/source/extensions/omni.isaac.lab/test/sim/test_simulation_render_config.py b/source/extensions/omni.isaac.lab/test/sim/test_simulation_render_config.py new file mode 100644 index 0000000000..cd885a75a0 --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/sim/test_simulation_render_config.py @@ -0,0 +1,154 @@ +# 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 +app_launcher = AppLauncher(headless=True, enable_cameras=True) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import unittest + +import carb + +from omni.isaac.lab.sim.simulation_cfg import RenderCfg, SimulationCfg +from omni.isaac.lab.sim.simulation_context import SimulationContext + + +class TestSimulationRenderConfig(unittest.TestCase): + """Tests for simulation context render config.""" + + """ + Tests + """ + + def test_render_cfg(self): + """Test that the simulation context is created with the correct render cfg.""" + enable_translucency = True + enable_reflections = True + enable_global_illumination = True + antialiasing_mode = "DLAA" + enable_dlssg = True + dlss_mode = 3 + enable_direct_lighting = True + samples_per_pixel = 4 + enable_shadows = True + enable_ambient_occlusion = True + + render_cfg = RenderCfg( + enable_translucency=enable_translucency, + enable_reflections=enable_reflections, + enable_global_illumination=enable_global_illumination, + antialiasing_mode=antialiasing_mode, + enable_dlssg=enable_dlssg, + dlss_mode=dlss_mode, + enable_direct_lighting=enable_direct_lighting, + samples_per_pixel=samples_per_pixel, + enable_shadows=enable_shadows, + enable_ambient_occlusion=enable_ambient_occlusion, + ) + + cfg = SimulationCfg(render=render_cfg) + + sim = SimulationContext(cfg) + + self.assertEqual(sim.cfg.render.enable_translucency, enable_translucency) + self.assertEqual(sim.cfg.render.enable_reflections, enable_reflections) + self.assertEqual(sim.cfg.render.enable_global_illumination, enable_global_illumination) + self.assertEqual(sim.cfg.render.antialiasing_mode, antialiasing_mode) + self.assertEqual(sim.cfg.render.enable_dlssg, enable_dlssg) + self.assertEqual(sim.cfg.render.dlss_mode, dlss_mode) + self.assertEqual(sim.cfg.render.enable_direct_lighting, enable_direct_lighting) + self.assertEqual(sim.cfg.render.samples_per_pixel, samples_per_pixel) + self.assertEqual(sim.cfg.render.enable_shadows, enable_shadows) + self.assertEqual(sim.cfg.render.enable_ambient_occlusion, enable_ambient_occlusion) + + carb_settings_iface = carb.settings.get_settings() + self.assertEqual(carb_settings_iface.get("/rtx/translucency/enabled"), sim.cfg.render.enable_translucency) + self.assertEqual(carb_settings_iface.get("/rtx/reflections/enabled"), sim.cfg.render.enable_reflections) + self.assertEqual( + carb_settings_iface.get("/rtx/indirectDiffuse/enabled"), sim.cfg.render.enable_global_illumination + ) + self.assertEqual(carb_settings_iface.get("/rtx/transient/dlssg/enabled"), sim.cfg.render.enable_dlssg) + self.assertEqual(carb_settings_iface.get("/rtx/post/dlss/execMode"), sim.cfg.render.dlss_mode) + self.assertEqual(carb_settings_iface.get("/rtx/directLighting/enabled"), sim.cfg.render.enable_direct_lighting) + self.assertEqual( + carb_settings_iface.get("/rtx/directLighting/sampledLighting/samplesPerPixel"), + sim.cfg.render.samples_per_pixel, + ) + self.assertEqual(carb_settings_iface.get("/rtx/shadows/enabled"), sim.cfg.render.enable_shadows) + self.assertEqual( + carb_settings_iface.get("/rtx/ambientOcclusion/enabled"), sim.cfg.render.enable_ambient_occlusion + ) + self.assertEqual(carb_settings_iface.get("/rtx/post/aa/op"), 4) # dlss = 3, dlaa=4 + + def test_render_cfg_defaults(self): + """Test that the simulation context is created with the correct render cfg.""" + enable_translucency = False + enable_reflections = False + enable_global_illumination = False + antialiasing_mode = "DLSS" + enable_dlssg = False + dlss_mode = 0 + enable_direct_lighting = False + samples_per_pixel = 1 + enable_shadows = False + enable_ambient_occlusion = False + + render_cfg = RenderCfg( + enable_translucency=enable_translucency, + enable_reflections=enable_reflections, + enable_global_illumination=enable_global_illumination, + antialiasing_mode=antialiasing_mode, + enable_dlssg=enable_dlssg, + dlss_mode=dlss_mode, + enable_direct_lighting=enable_direct_lighting, + samples_per_pixel=samples_per_pixel, + enable_shadows=enable_shadows, + enable_ambient_occlusion=enable_ambient_occlusion, + ) + + cfg = SimulationCfg(render=render_cfg) + + sim = SimulationContext(cfg) + + self.assertEqual(sim.cfg.render.enable_translucency, enable_translucency) + self.assertEqual(sim.cfg.render.enable_reflections, enable_reflections) + self.assertEqual(sim.cfg.render.enable_global_illumination, enable_global_illumination) + self.assertEqual(sim.cfg.render.antialiasing_mode, antialiasing_mode) + self.assertEqual(sim.cfg.render.enable_dlssg, enable_dlssg) + self.assertEqual(sim.cfg.render.dlss_mode, dlss_mode) + self.assertEqual(sim.cfg.render.enable_direct_lighting, enable_direct_lighting) + self.assertEqual(sim.cfg.render.samples_per_pixel, samples_per_pixel) + self.assertEqual(sim.cfg.render.enable_shadows, enable_shadows) + self.assertEqual(sim.cfg.render.enable_ambient_occlusion, enable_ambient_occlusion) + + carb_settings_iface = carb.settings.get_settings() + self.assertEqual(carb_settings_iface.get("/rtx/translucency/enabled"), sim.cfg.render.enable_translucency) + self.assertEqual(carb_settings_iface.get("/rtx/reflections/enabled"), sim.cfg.render.enable_reflections) + self.assertEqual( + carb_settings_iface.get("/rtx/indirectDiffuse/enabled"), sim.cfg.render.enable_global_illumination + ) + self.assertEqual(carb_settings_iface.get("/rtx/transient/dlssg/enabled"), sim.cfg.render.enable_dlssg) + self.assertEqual(carb_settings_iface.get("/rtx/post/dlss/execMode"), sim.cfg.render.dlss_mode) + self.assertEqual(carb_settings_iface.get("/rtx/directLighting/enabled"), sim.cfg.render.enable_direct_lighting) + self.assertEqual( + carb_settings_iface.get("/rtx/directLighting/sampledLighting/samplesPerPixel"), + sim.cfg.render.samples_per_pixel, + ) + self.assertEqual(carb_settings_iface.get("/rtx/shadows/enabled"), sim.cfg.render.enable_shadows) + self.assertEqual( + carb_settings_iface.get("/rtx/ambientOcclusion/enabled"), sim.cfg.render.enable_ambient_occlusion + ) + self.assertEqual(carb_settings_iface.get("/rtx/post/aa/op"), 3) # dlss = 3, dlaa=4 + + +if __name__ == "__main__": + run_tests() diff --git a/source/extensions/omni.isaac.lab/test/sim/test_spawn_sensors.py b/source/extensions/omni.isaac.lab/test/sim/test_spawn_sensors.py index 328d69367c..8e9f9a556c 100644 --- a/source/extensions/omni.isaac.lab/test/sim/test_spawn_sensors.py +++ b/source/extensions/omni.isaac.lab/test/sim/test_spawn_sensors.py @@ -69,7 +69,7 @@ def test_spawn_pinhole_camera(self): def test_spawn_fisheye_camera(self): """Test spawning a fisheye camera.""" cfg = sim_utils.FisheyeCameraCfg( - projection_type="fisheye_equidistant", + projection_type="fisheyePolynomial", focal_length=5.0, f_stop=10.0, clipping_range=(0.1, 1000.0), 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/extensions/omni.isaac.lab/test/utils/test_configclass.py b/source/extensions/omni.isaac.lab/test/utils/test_configclass.py index 1ee984ce52..4b2f5a7ff1 100644 --- a/source/extensions/omni.isaac.lab/test/utils/test_configclass.py +++ b/source/extensions/omni.isaac.lab/test/utils/test_configclass.py @@ -23,7 +23,7 @@ from collections.abc import Callable from dataclasses import MISSING, asdict, field from functools import wraps -from typing import ClassVar +from typing import Any, ClassVar from omni.isaac.lab.utils.configclass import configclass from omni.isaac.lab.utils.dict import class_to_dict, dict_to_md5_hash, update_class_from_dict @@ -85,6 +85,11 @@ def double(x): return 2 * x +@configclass +class ModifierCfg: + params: dict[str, Any] = {"A": 1, "B": 2} + + @configclass class ViewerCfg: eye: list = [7.5, 7.5, 7.5] # field missing on purpose @@ -113,6 +118,7 @@ class BasicDemoCfg: device_id: int = 0 env: EnvCfg = EnvCfg() robot_default_state: RobotDefaultStateCfg = RobotDefaultStateCfg() + list_config = [ModifierCfg(), ModifierCfg(params={"A": 3, "B": 4})] @configclass @@ -329,6 +335,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 """ @@ -342,6 +387,7 @@ class NestedDictAndListCfg: "dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], }, "device_id": 0, + "list_config": [{"params": {"A": 1, "B": 2}}, {"params": {"A": 3, "B": 4}}], } basic_demo_cfg_change_correct = { @@ -353,6 +399,7 @@ class NestedDictAndListCfg: "dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], }, "device_id": 0, + "list_config": [{"params": {"A": 1, "B": 2}}, {"params": {"A": 3, "B": 4}}], } basic_demo_cfg_change_with_none_correct = { @@ -364,6 +411,19 @@ class NestedDictAndListCfg: "dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], }, "device_id": 0, + "list_config": [{"params": {"A": 1, "B": 2}}, {"params": {"A": 3, "B": 4}}], +} + +basic_demo_cfg_change_with_tuple_correct = { + "env": {"num_envs": 56, "episode_length": 2000, "viewer": {"eye": [7.5, 7.5, 7.5], "lookat": [0.0, 0.0, 0.0]}}, + "robot_default_state": { + "pos": (0.0, 0.0, 0.0), + "rot": (1.0, 0.0, 0.0, 0.0), + "dof_pos": (0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + "dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + }, + "device_id": 0, + "list_config": [{"params": {"A": -1, "B": -2}}, {"params": {"A": -3, "B": -4}}], } basic_demo_cfg_nested_dict_and_list = { @@ -404,6 +464,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. """ @@ -441,7 +517,7 @@ def test_dict_conversion(self): def test_dict_conversion_order(self): """Tests that order is conserved when converting to dictionary.""" - true_outer_order = ["device_id", "env", "robot_default_state"] + true_outer_order = ["device_id", "env", "robot_default_state", "list_config"] true_env_order = ["num_envs", "episode_length", "viewer"] # create config cfg = BasicDemoCfg() @@ -459,7 +535,7 @@ def test_dict_conversion_order(self): self.assertEqual(label, parsed_value) # check ordering when copied cfg_dict_copied = copy.deepcopy(cfg_dict) - cfg_dict_copied.pop("robot_default_state") + cfg_dict_copied.pop("list_config") # check ordering for label, parsed_value in zip(true_outer_order, cfg_dict_copied.keys()): self.assertEqual(label, parsed_value) @@ -496,6 +572,13 @@ def test_config_update_dict_with_none(self): update_class_from_dict(cfg, cfg_dict) self.assertDictEqual(asdict(cfg), basic_demo_cfg_change_with_none_correct) + def test_config_update_dict_tuple(self): + """Test updating configclass using a dictionary that modifies a tuple.""" + cfg = BasicDemoCfg() + cfg_dict = {"list_config": [{"params": {"A": -1, "B": -2}}, {"params": {"A": -3, "B": -4}}]} + update_class_from_dict(cfg, cfg_dict) + self.assertDictEqual(asdict(cfg), basic_demo_cfg_change_with_tuple_correct) + def test_config_update_nested_dict(self): """Test updating configclass with sub-dictionaries.""" cfg = NestedDictAndListCfg() @@ -888,6 +971,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/test/utils/test_episode_data.py b/source/extensions/omni.isaac.lab/test/utils/test_episode_data.py new file mode 100644 index 0000000000..b3cd1849e0 --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/utils/test_episode_data.py @@ -0,0 +1,140 @@ +# Copyright (c) 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 in headless mode +simulation_app = AppLauncher(headless=True).app + +"""Rest everything follows from here.""" + +import torch +import unittest + +from omni.isaac.lab.utils.datasets import EpisodeData + + +class TestEpisodeData(unittest.TestCase): + """Test EpisodeData implementation.""" + + """ + Test cases for EpisodeData class. + """ + + def test_is_empty(self): + """Test checking whether the episode is empty.""" + for device in ("cuda:0", "cpu"): + with self.subTest(device=device): + episode = EpisodeData() + self.assertTrue(episode.is_empty()) + + episode.add("key", torch.tensor([1, 2, 3], device=device)) + self.assertFalse(episode.is_empty()) + + def test_add_tensors(self): + """Test appending tensor data to the episode.""" + for device in ("cuda:0", "cpu"): + with self.subTest(device=device): + dummy_data_0 = torch.tensor([0], device=device) + dummy_data_1 = torch.tensor([1], device=device) + expected_added_data = torch.cat((dummy_data_0.unsqueeze(0), dummy_data_1.unsqueeze(0))) + episode = EpisodeData() + + # test adding data to a key that does not exist + episode.add("key", dummy_data_0) + self.assertTrue(torch.equal(episode.data.get("key"), dummy_data_0.unsqueeze(0))) + + # test adding data to a key that exists + episode.add("key", dummy_data_1) + self.assertTrue(torch.equal(episode.data.get("key"), expected_added_data)) + + # test adding data to a key with "/" in the name + episode.add("first/second", dummy_data_0) + self.assertTrue(torch.equal(episode.data.get("first").get("second"), dummy_data_0.unsqueeze(0))) + + # test adding data to a key with "/" in the name that already exists + episode.add("first/second", dummy_data_1) + self.assertTrue(torch.equal(episode.data.get("first").get("second"), expected_added_data)) + + def test_add_dict_tensors(self): + """Test appending dict data to the episode.""" + for device in ("cuda:0", "cpu"): + with self.subTest(device=device): + dummy_dict_data_0 = { + "key_0": torch.tensor([0], device=device), + "key_1": {"key_1_0": torch.tensor([1], device=device), "key_1_1": torch.tensor([2], device=device)}, + } + dummy_dict_data_1 = { + "key_0": torch.tensor([3], device=device), + "key_1": {"key_1_0": torch.tensor([4], device=device), "key_1_1": torch.tensor([5], device=device)}, + } + + episode = EpisodeData() + + # test adding dict data to a key that does not exist + episode.add("key", dummy_dict_data_0) + self.assertTrue(torch.equal(episode.data.get("key").get("key_0"), torch.tensor([[0]], device=device))) + self.assertTrue( + torch.equal(episode.data.get("key").get("key_1").get("key_1_0"), torch.tensor([[1]], device=device)) + ) + self.assertTrue( + torch.equal(episode.data.get("key").get("key_1").get("key_1_1"), torch.tensor([[2]], device=device)) + ) + + # test adding dict data to a key that exists + episode.add("key", dummy_dict_data_1) + self.assertTrue( + torch.equal(episode.data.get("key").get("key_0"), torch.tensor([[0], [3]], device=device)) + ) + self.assertTrue( + torch.equal( + episode.data.get("key").get("key_1").get("key_1_0"), torch.tensor([[1], [4]], device=device) + ) + ) + self.assertTrue( + torch.equal( + episode.data.get("key").get("key_1").get("key_1_1"), torch.tensor([[2], [5]], device=device) + ) + ) + + def test_get_initial_state(self): + """Test getting the initial state of the episode.""" + for device in ("cuda:0", "cpu"): + with self.subTest(device=device): + dummy_initial_state = torch.tensor([1, 2, 3], device=device) + episode = EpisodeData() + + episode.add("initial_state", dummy_initial_state) + self.assertTrue(torch.equal(episode.get_initial_state(), dummy_initial_state.unsqueeze(0))) + + def test_get_next_action(self): + """Test getting next actions.""" + for device in ("cuda:0", "cpu"): + with self.subTest(device=device): + # dummy actions + action1 = torch.tensor([1, 2, 3], device=device) + action2 = torch.tensor([4, 5, 6], device=device) + action3 = torch.tensor([7, 8, 9], device=device) + + episode = EpisodeData() + self.assertIsNone(episode.get_next_action()) + + episode.add("actions", action1) + episode.add("actions", action2) + episode.add("actions", action3) + + # check if actions are returned in the correct order + self.assertTrue(torch.equal(episode.get_next_action(), action1)) + self.assertTrue(torch.equal(episode.get_next_action(), action2)) + self.assertTrue(torch.equal(episode.get_next_action(), action3)) + + # check if None is returned when all actions are exhausted + self.assertIsNone(episode.get_next_action()) + + +if __name__ == "__main__": + run_tests() diff --git a/source/extensions/omni.isaac.lab/test/utils/test_hdf5_dataset_file_handler.py b/source/extensions/omni.isaac.lab/test/utils/test_hdf5_dataset_file_handler.py new file mode 100644 index 0000000000..64af6a9e0c --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/utils/test_hdf5_dataset_file_handler.py @@ -0,0 +1,129 @@ +# Copyright (c) 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 in headless mode +simulation_app = AppLauncher(headless=True).app + +"""Rest everything follows from here.""" + +import os +import shutil +import tempfile +import torch +import unittest +import uuid + +from omni.isaac.lab.utils.datasets import EpisodeData, HDF5DatasetFileHandler + + +def create_test_episode(device): + """create a test episode with dummy data.""" + test_episode = EpisodeData() + + test_episode.seed = 0 + test_episode.success = True + + test_episode.add("initial_state", torch.tensor([1, 2, 3], device=device)) + + test_episode.add("actions", torch.tensor([1, 2, 3], device=device)) + test_episode.add("actions", torch.tensor([4, 5, 6], device=device)) + test_episode.add("actions", torch.tensor([7, 8, 9], device=device)) + + test_episode.add("obs/policy/term1", torch.tensor([1, 2, 3, 4, 5], device=device)) + test_episode.add("obs/policy/term1", torch.tensor([6, 7, 8, 9, 10], device=device)) + test_episode.add("obs/policy/term1", torch.tensor([11, 12, 13, 14, 15], device=device)) + + return test_episode + + +class TestHDF5DatasetFileHandler(unittest.TestCase): + """Test HDF5 dataset filer handler implementation.""" + + """ + Test cases for HDF5DatasetFileHandler class. + """ + + def setUp(self): + # create a temporary directory to store the test datasets + self.temp_dir = tempfile.mkdtemp() + + def tearDown(self): + # delete the temporary directory after the test + shutil.rmtree(self.temp_dir) + + def test_create_dataset_file(self): + """Test creating a new dataset file.""" + # create a dataset file given a file name with extension + dataset_file_path = os.path.join(self.temp_dir, f"{uuid.uuid4()}.hdf5") + dataset_file_handler = HDF5DatasetFileHandler() + dataset_file_handler.create(dataset_file_path, "test_env_name") + dataset_file_handler.close() + + # check if the dataset is created + self.assertTrue(os.path.exists(dataset_file_path)) + + # create a dataset file given a file name without extension + dataset_file_path = os.path.join(self.temp_dir, f"{uuid.uuid4()}") + dataset_file_handler = HDF5DatasetFileHandler() + dataset_file_handler.create(dataset_file_path, "test_env_name") + dataset_file_handler.close() + + # check if the dataset is created + self.assertTrue(os.path.exists(dataset_file_path + ".hdf5")) + + def test_write_and_load_episode(self): + """Test writing and loading an episode to and from the dataset file.""" + for device in ("cuda:0", "cpu"): + with self.subTest(device=device): + dataset_file_path = os.path.join(self.temp_dir, f"{uuid.uuid4()}.hdf5") + dataset_file_handler = HDF5DatasetFileHandler() + dataset_file_handler.create(dataset_file_path, "test_env_name") + + test_episode = create_test_episode(device) + + # write the episode to the dataset + dataset_file_handler.write_episode(test_episode) + dataset_file_handler.flush() + + self.assertEqual(dataset_file_handler.get_num_episodes(), 1) + + # write the episode again to test writing 2nd episode + dataset_file_handler.write_episode(test_episode) + dataset_file_handler.flush() + + self.assertEqual(dataset_file_handler.get_num_episodes(), 2) + + # close the dataset file to prepare for testing the load function + dataset_file_handler.close() + + # load the episode from the dataset + dataset_file_handler = HDF5DatasetFileHandler() + dataset_file_handler.open(dataset_file_path) + + self.assertEqual(dataset_file_handler.get_env_name(), "test_env_name") + + loaded_episode_names = dataset_file_handler.get_episode_names() + self.assertEqual(len(list(loaded_episode_names)), 2) + + for episode_name in loaded_episode_names: + loaded_episode = dataset_file_handler.load_episode(episode_name, device=device) + self.assertEqual(loaded_episode.env_id, "test_env_name") + self.assertEqual(loaded_episode.seed, test_episode.seed) + self.assertEqual(loaded_episode.success, test_episode.success) + + self.assertTrue(torch.equal(loaded_episode.get_initial_state(), test_episode.get_initial_state())) + + for action in test_episode.data["actions"]: + self.assertTrue(torch.equal(loaded_episode.get_next_action(), action)) + + dataset_file_handler.close() + + +if __name__ == "__main__": + run_tests() diff --git a/source/extensions/omni.isaac.lab/test/utils/test_math.py b/source/extensions/omni.isaac.lab/test/utils/test_math.py index d6d6086290..33a1cb1497 100644 --- a/source/extensions/omni.isaac.lab/test/utils/test_math.py +++ b/source/extensions/omni.isaac.lab/test/utils/test_math.py @@ -195,6 +195,43 @@ def test_quat_error_mag_with_quat_unique(self): torch.testing.assert_close(error_3, error_4) torch.testing.assert_close(error_4, error_1) + def test_convention_converter(self): + """Test convert_camera_frame_orientation_convention to and from ros, opengl, and world conventions.""" + quat_ros = torch.tensor([[-0.17591989, 0.33985114, 0.82047325, -0.42470819]]) + quat_opengl = torch.tensor([[0.33985113, 0.17591988, 0.42470818, 0.82047324]]) + quat_world = torch.tensor([[-0.3647052, -0.27984815, -0.1159169, 0.88047623]]) + + # from ROS + torch.testing.assert_close( + math_utils.convert_camera_frame_orientation_convention(quat_ros, "ros", "opengl"), quat_opengl + ) + torch.testing.assert_close( + math_utils.convert_camera_frame_orientation_convention(quat_ros, "ros", "world"), quat_world + ) + torch.testing.assert_close( + math_utils.convert_camera_frame_orientation_convention(quat_ros, "ros", "ros"), quat_ros + ) + # from OpenGL + torch.testing.assert_close( + math_utils.convert_camera_frame_orientation_convention(quat_opengl, "opengl", "ros"), quat_ros + ) + torch.testing.assert_close( + math_utils.convert_camera_frame_orientation_convention(quat_opengl, "opengl", "world"), quat_world + ) + torch.testing.assert_close( + math_utils.convert_camera_frame_orientation_convention(quat_opengl, "opengl", "opengl"), quat_opengl + ) + # from World + torch.testing.assert_close( + math_utils.convert_camera_frame_orientation_convention(quat_world, "world", "ros"), quat_ros + ) + torch.testing.assert_close( + math_utils.convert_camera_frame_orientation_convention(quat_world, "world", "opengl"), quat_opengl + ) + torch.testing.assert_close( + math_utils.convert_camera_frame_orientation_convention(quat_world, "world", "world"), quat_world + ) + def test_wrap_to_pi(self): """Test wrap_to_pi method.""" # Define test cases diff --git a/source/extensions/omni.isaac.lab/test/utils/test_noise.py b/source/extensions/omni.isaac.lab/test/utils/test_noise.py new file mode 100644 index 0000000000..f5355d0856 --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/utils/test_noise.py @@ -0,0 +1,118 @@ +# 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 +app_launcher = AppLauncher(headless=True) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import torch +import unittest + +import omni.isaac.lab.utils.noise as noise + + +class TestNoise(unittest.TestCase): + """Test different noise implementations.""" + + def test_gaussian_noise(self): + """Test guassian_noise function.""" + + for device in ["cpu", "cuda"]: + for noise_device in ["cpu", "cuda"]: + for op in ["add", "scale", "abs"]: + with self.subTest(device=device, noise_device=noise_device, operation=op): + # create random data set + data = torch.rand(10000, 3, device=device) + # define standard deviation and mean + std = torch.tensor([0.1, 0.2, 0.3], device=noise_device) + mean = torch.tensor([0.4, 0.5, 0.6], device=noise_device) + # create noise config + noise_cfg = noise.GaussianNoiseCfg(std=std, mean=mean, operation=op) + + for i in range(10): + # apply noise + noisy_data = noise_cfg.func(data, cfg=noise_cfg) + # calculate resulting noise compared to original data set + if op == "add": + std_result, mean_result = torch.std_mean(noisy_data - data, dim=0) + elif op == "scale": + std_result, mean_result = torch.std_mean(noisy_data / data, dim=0) + elif op == "abs": + std_result, mean_result = torch.std_mean(noisy_data, dim=0) + + self.assertTrue(noise_cfg.mean.device, device) + self.assertTrue(noise_cfg.std.device, device) + torch.testing.assert_close(noise_cfg.std, std_result, atol=1e-2, rtol=1e-2) + torch.testing.assert_close(noise_cfg.mean, mean_result, atol=1e-2, rtol=1e-2) + + def test_uniform_noise(self): + """Test uniform_noise function.""" + for device in ["cpu", "cuda"]: + for noise_device in ["cpu", "cuda"]: + for op in ["add", "scale", "abs"]: + with self.subTest(device=device, noise_device=noise_device, operation=op): + # create random data set + data = torch.rand(10000, 3, device=device) + # define uniform minimum and maximum + n_min = torch.tensor([0.1, 0.2, 0.3], device=noise_device) + n_max = torch.tensor([0.4, 0.5, 0.6], device=noise_device) + # create noise config + noise_cfg = noise.UniformNoiseCfg(n_max=n_max, n_min=n_min, operation=op) + + for i in range(10): + # apply noise + noisy_data = noise_cfg.func(data, cfg=noise_cfg) + # calculate resulting noise compared to original data set + if op == "add": + min_result, _ = torch.min(noisy_data - data, dim=0) + max_result, _ = torch.max(noisy_data - data, dim=0) + elif op == "scale": + min_result, _ = torch.min(torch.div(noisy_data, data), dim=0) + max_result, _ = torch.max(torch.div(noisy_data, data), dim=0) + elif op == "abs": + min_result, _ = torch.min(noisy_data, dim=0) + max_result, _ = torch.max(noisy_data, dim=0) + + self.assertTrue(noise_cfg.n_min.device, device) + self.assertTrue(noise_cfg.n_max.device, device) + self.assertTrue(all(torch.le(noise_cfg.n_min, min_result).tolist())) + self.assertTrue(all(torch.ge(noise_cfg.n_max, max_result).tolist())) + + def test_constant_noise(self): + """Test constant_noise""" + for device in ["cpu", "cuda"]: + for noise_device in ["cpu", "cuda"]: + for op in ["add", "scale", "abs"]: + with self.subTest(device=device, noise_device=noise_device, operation=op): + # create random data set + data = torch.rand(10000, 3, device=device) + # define a bias + bias = torch.tensor([0.1, 0.2, 0.3], device=noise_device) + # create noise config + noise_cfg = noise.ConstantNoiseCfg(bias=bias, operation=op) + + for i in range(10): + # apply noise + noisy_data = noise_cfg.func(data, cfg=noise_cfg) + # calculate resulting noise compared to original data set + if op == "add": + bias_result = noisy_data - data + elif op == "scale": + bias_result = noisy_data / data + elif op == "abs": + bias_result = noisy_data + + self.assertTrue(noise_cfg.bias.device, device) + torch.testing.assert_close(noise_cfg.bias.repeat(data.shape[0], 1), bias_result) + + +if __name__ == "__main__": + run_tests() diff --git a/source/extensions/omni.isaac.lab_assets/omni/isaac/lab_assets/unitree.py b/source/extensions/omni.isaac.lab_assets/omni/isaac/lab_assets/unitree.py index e13722ae59..d0eb236dae 100644 --- a/source/extensions/omni.isaac.lab_assets/omni/isaac/lab_assets/unitree.py +++ b/source/extensions/omni.isaac.lab_assets/omni/isaac/lab_assets/unitree.py @@ -11,7 +11,9 @@ * :obj:`UNITREE_GO1_CFG`: Unitree Go1 robot with actuator net model for the legs * :obj:`UNITREE_GO2_CFG`: Unitree Go2 robot with DC motor model for the legs * :obj:`H1_CFG`: H1 humanoid robot +* :obj:`H1_MINIMAL_CFG`: H1 humanoid robot with minimal collision bodies * :obj:`G1_CFG`: G1 humanoid robot +* :obj:`G1_MINIMAL_CFG`: G1 humanoid robot with minimal collision bodies Reference: https://github.com/unitreerobotics/unitree_ros """ diff --git a/source/extensions/omni.isaac.lab_tasks/config/extension.toml b/source/extensions/omni.isaac.lab_tasks/config/extension.toml index 89ca646936..ba976faf92 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.5" +version = "0.10.14" # 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 2614630bd7..068db9b64a 100644 --- a/source/extensions/omni.isaac.lab_tasks/docs/CHANGELOG.rst +++ b/source/extensions/omni.isaac.lab_tasks/docs/CHANGELOG.rst @@ -1,11 +1,91 @@ Changelog --------- +0.10.14 (2024-12-03) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added ``Isaac-Stack-Cube-Franka-IK-Rel-v0`` and ``Isaac-Stack-Cube-Instance-Randomize-Franka-IK-Rel-v0`` environments + as manager-based RL envs that implement a three cube stacking task. + +0.10.13 (2024-10-30) +~~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Defined the Gymnasium task entry points with configuration strings instead of class types. + This avoids unnecessary imports and improves the load types. +* Blacklisted ``mdp`` directories during the recursive module search. + + +0.10.12 (2024-10-28) +~~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Changed manager-based vision cartpole environment names from Isaac-Cartpole-RGB-Camera-v0 + and Isaac-Cartpole-Depth-Camera-v0 to Isaac-Cartpole-RGB-v0 and Isaac-Cartpole-Depth-v0 + + +0.10.11 (2024-10-28) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added feature extracted observation cartpole examples. + +0.10.10 (2024-10-25) +~~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed issues with defining Gymnasium spaces in Direct workflows due to Hydra/OmegaConf limitations with non-primitive types. + + +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) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed the wrong selection of body id's in the :meth:`omni.isaac.lab_tasks.manager_based.locomotion.velocity.mdp.rewards.feet_slide` + reward function. This makes sure the right IDs are selected for the bodies. + + +0.10.7 (2024-10-02) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Replace deprecated :attr:`num_observations`, :attr:`num_actions` and :attr:`num_states` in single-agent direct tasks + by :attr:`observation_space`, :attr:`action_space` and :attr:`state_space` respectively. +* Replace deprecated :attr:`num_observations`, :attr:`num_actions` and :attr:`num_states` in multi-agent direct tasks + by :attr:`observation_spaces`, :attr:`action_spaces` and :attr:`state_space` respectively. + + 0.10.6 (2024-09-25) ~~~~~~~~~~~~~~~~~~~ Added ^^^^^ + * Added ``Isaac-Cartpole-RGB-Camera-v0`` and ``Isaac-Cartpole-Depth-Camera-v0`` manager based camera cartpole environments. diff --git a/source/extensions/omni.isaac.lab_tasks/docs/README.md b/source/extensions/omni.isaac.lab_tasks/docs/README.md index fe225ca25a..ce9dfdb77d 100644 --- a/source/extensions/omni.isaac.lab_tasks/docs/README.md +++ b/source/extensions/omni.isaac.lab_tasks/docs/README.md @@ -32,16 +32,26 @@ The environments are then registered in the `omni/isaac/lab_tasks/locomotion/vel ```python gym.register( id="Isaac-Velocity-Rough-Anymal-C-v0", - entry_point="omni.isaac.lab.envs:RLTaskEnv", + entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, - kwargs={"env_cfg_entry_point": f"{__name__}.rough_env_cfg:AnymalCRoughEnvCfg"}, + kwargs={ + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:AnymalCRoughEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_rough_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalCRoughPPORunnerCfg", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", + }, ) gym.register( id="Isaac-Velocity-Flat-Anymal-C-v0", - entry_point="omni.isaac.lab.envs:RLTaskEnv", + entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, - kwargs={"env_cfg_entry_point": f"{__name__}.flat_env_cfg:AnymalCFlatEnvCfg"}, + kwargs={ + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:AnymalCFlatEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalCFlatPPORunnerCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_flat_ppo_cfg.yaml", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", + }, ) ``` diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/__init__.py index 5dd39864d3..6f06dbcd02 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/__init__.py @@ -25,6 +25,6 @@ from .utils import import_packages # The blacklist is used to prevent importing configs from sub-packages -_BLACKLIST_PKGS = ["utils"] +_BLACKLIST_PKGS = ["utils", ".mdp"] # Import all configs in this package import_packages(__name__, _BLACKLIST_PKGS) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/allegro_hand/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/allegro_hand/__init__.py index 0f4ed4544f..b0734779f5 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/allegro_hand/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/allegro_hand/__init__.py @@ -10,18 +10,19 @@ import gymnasium as gym from . import agents -from .allegro_hand_env_cfg import AllegroHandEnvCfg ## # Register Gym environments. ## +inhand_task_entry = "omni.isaac.lab_tasks.direct.inhand_manipulation" + gym.register( id="Isaac-Repose-Cube-Allegro-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.inhand_manipulation:InHandManipulationEnv", + entry_point=f"{inhand_task_entry}.inhand_manipulation_env:InHandManipulationEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": AllegroHandEnvCfg, + "env_cfg_entry_point": f"{__name__}.allegro_hand_env_cfg:AllegroHandEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AllegroHandPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/allegro_hand/allegro_hand_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/allegro_hand/allegro_hand_env_cfg.py index b83b6782a6..b5c53a91d3 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/allegro_hand/allegro_hand_env_cfg.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/allegro_hand/allegro_hand_env_cfg.py @@ -22,9 +22,9 @@ class AllegroHandEnvCfg(DirectRLEnvCfg): # env decimation = 4 episode_length_s = 10.0 - num_actions = 16 - num_observations = 124 # (full) - num_states = 0 + action_space = 16 + observation_space = 124 # (full) + state_space = 0 asymmetric_obs = False obs_type = "full" # simulation diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/ant/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/ant/__init__.py index 97a3814e6c..940702a105 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/ant/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/ant/__init__.py @@ -10,7 +10,6 @@ import gymnasium as gym from . import agents -from .ant_env import AntEnv, AntEnvCfg ## # Register Gym environments. @@ -18,10 +17,10 @@ gym.register( id="Isaac-Ant-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.ant:AntEnv", + entry_point=f"{__name__}.ant_env:AntEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": AntEnvCfg, + "env_cfg_entry_point": f"{__name__}.ant_env:AntEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AntPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/ant/ant_env.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/ant/ant_env.py index 8bf6d6bcc9..42f57127ee 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/ant/ant_env.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/ant/ant_env.py @@ -24,9 +24,9 @@ class AntEnvCfg(DirectRLEnvCfg): episode_length_s = 15.0 decimation = 2 action_scale = 0.5 - num_actions = 8 - num_observations = 36 - num_states = 0 + action_space = 8 + observation_space = 36 + state_space = 0 # simulation sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/anymal_c/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/anymal_c/__init__.py index 1a245b7293..8160e044d9 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/anymal_c/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/anymal_c/__init__.py @@ -10,7 +10,6 @@ import gymnasium as gym from . import agents -from .anymal_c_env import AnymalCEnv, AnymalCFlatEnvCfg, AnymalCRoughEnvCfg ## # Register Gym environments. @@ -18,10 +17,10 @@ gym.register( id="Isaac-Velocity-Flat-Anymal-C-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.anymal_c:AnymalCEnv", + entry_point=f"{__name__}.anymal_c_env:AnymalCEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": AnymalCFlatEnvCfg, + "env_cfg_entry_point": f"{__name__}.anymal_c_env_cfg:AnymalCFlatEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_flat_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalCFlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", @@ -30,10 +29,10 @@ gym.register( id="Isaac-Velocity-Rough-Anymal-C-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.anymal_c:AnymalCEnv", + entry_point=f"{__name__}.anymal_c_env:AnymalCEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": AnymalCRoughEnvCfg, + "env_cfg_entry_point": f"{__name__}.anymal_c_env_cfg:AnymalCRoughEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_rough_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalCRoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/anymal_c/anymal_c_env.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/anymal_c/anymal_c_env.py index 5490bb0dd3..1b2ee577c7 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/anymal_c/anymal_c_env.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/anymal_c/anymal_c_env.py @@ -5,152 +5,15 @@ from __future__ import annotations +import gymnasium as gym import torch -import omni.isaac.lab.envs.mdp as mdp import omni.isaac.lab.sim as sim_utils -from omni.isaac.lab.assets import Articulation, ArticulationCfg -from omni.isaac.lab.envs import DirectRLEnv, DirectRLEnvCfg -from omni.isaac.lab.managers import EventTermCfg as EventTerm -from omni.isaac.lab.managers import SceneEntityCfg -from omni.isaac.lab.scene import InteractiveSceneCfg -from omni.isaac.lab.sensors import ContactSensor, ContactSensorCfg, RayCaster, RayCasterCfg, patterns -from omni.isaac.lab.sim import SimulationCfg -from omni.isaac.lab.terrains import TerrainImporterCfg -from omni.isaac.lab.utils import configclass +from omni.isaac.lab.assets import Articulation +from omni.isaac.lab.envs import DirectRLEnv +from omni.isaac.lab.sensors import ContactSensor, RayCaster -## -# Pre-defined configs -## -from omni.isaac.lab_assets.anymal import ANYMAL_C_CFG # isort: skip -from omni.isaac.lab.terrains.config.rough import ROUGH_TERRAINS_CFG # isort: skip - - -@configclass -class EventCfg: - """Configuration for randomization.""" - - physics_material = EventTerm( - func=mdp.randomize_rigid_body_material, - mode="startup", - params={ - "asset_cfg": SceneEntityCfg("robot", body_names=".*"), - "static_friction_range": (0.8, 0.8), - "dynamic_friction_range": (0.6, 0.6), - "restitution_range": (0.0, 0.0), - "num_buckets": 64, - }, - ) - - add_base_mass = EventTerm( - func=mdp.randomize_rigid_body_mass, - mode="startup", - params={ - "asset_cfg": SceneEntityCfg("robot", body_names="base"), - "mass_distribution_params": (-5.0, 5.0), - "operation": "add", - }, - ) - - -@configclass -class AnymalCFlatEnvCfg(DirectRLEnvCfg): - # env - episode_length_s = 20.0 - decimation = 4 - action_scale = 0.5 - num_actions = 12 - num_observations = 48 - num_states = 0 - - # simulation - sim: SimulationCfg = SimulationCfg( - dt=1 / 200, - render_interval=decimation, - disable_contact_processing=True, - physics_material=sim_utils.RigidBodyMaterialCfg( - friction_combine_mode="multiply", - restitution_combine_mode="multiply", - static_friction=1.0, - dynamic_friction=1.0, - restitution=0.0, - ), - ) - terrain = TerrainImporterCfg( - prim_path="/World/ground", - terrain_type="plane", - collision_group=-1, - physics_material=sim_utils.RigidBodyMaterialCfg( - friction_combine_mode="multiply", - restitution_combine_mode="multiply", - static_friction=1.0, - dynamic_friction=1.0, - restitution=0.0, - ), - debug_vis=False, - ) - - # scene - scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=4096, env_spacing=4.0, replicate_physics=True) - - # events - events: EventCfg = EventCfg() - - # robot - robot: ArticulationCfg = ANYMAL_C_CFG.replace(prim_path="/World/envs/env_.*/Robot") - contact_sensor: ContactSensorCfg = ContactSensorCfg( - prim_path="/World/envs/env_.*/Robot/.*", history_length=3, update_period=0.005, track_air_time=True - ) - - # reward scales - lin_vel_reward_scale = 1.0 - yaw_rate_reward_scale = 0.5 - z_vel_reward_scale = -2.0 - ang_vel_reward_scale = -0.05 - joint_torque_reward_scale = -2.5e-5 - joint_accel_reward_scale = -2.5e-7 - action_rate_reward_scale = -0.01 - feet_air_time_reward_scale = 0.5 - undersired_contact_reward_scale = -1.0 - flat_orientation_reward_scale = -5.0 - - -@configclass -class AnymalCRoughEnvCfg(AnymalCFlatEnvCfg): - # env - num_observations = 235 - - terrain = TerrainImporterCfg( - prim_path="/World/ground", - terrain_type="generator", - terrain_generator=ROUGH_TERRAINS_CFG, - max_init_terrain_level=9, - collision_group=-1, - physics_material=sim_utils.RigidBodyMaterialCfg( - friction_combine_mode="multiply", - restitution_combine_mode="multiply", - static_friction=1.0, - dynamic_friction=1.0, - ), - visual_material=sim_utils.MdlFileCfg( - mdl_path="{NVIDIA_NUCLEUS_DIR}/Materials/Base/Architecture/Shingles_01.mdl", - project_uvw=True, - ), - debug_vis=False, - ) - - # we add a height scanner for perceptive locomotion - height_scanner = RayCasterCfg( - prim_path="/World/envs/env_.*/Robot/base", - offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)), - attach_yaw_only=True, - pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=[1.6, 1.0]), - debug_vis=False, - mesh_prim_paths=["/World/ground"], - ) - - # reward scales (override from flat config) - flat_orientation_reward_scale = 0.0 +from .anymal_c_env_cfg import AnymalCFlatEnvCfg, AnymalCRoughEnvCfg class AnymalCEnv(DirectRLEnv): @@ -160,8 +23,10 @@ def __init__(self, cfg: AnymalCFlatEnvCfg | AnymalCRoughEnvCfg, render_mode: str super().__init__(cfg, render_mode, **kwargs) # Joint position command (deviation from default joint positions) - self._actions = torch.zeros(self.num_envs, self.cfg.num_actions, device=self.device) - self._previous_actions = torch.zeros(self.num_envs, self.cfg.num_actions, device=self.device) + self._actions = torch.zeros(self.num_envs, gym.spaces.flatdim(self.single_action_space), device=self.device) + self._previous_actions = torch.zeros( + self.num_envs, gym.spaces.flatdim(self.single_action_space), device=self.device + ) # X/Y linear velocity and yaw angular velocity commands self._commands = torch.zeros(self.num_envs, 3, device=self.device) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/anymal_c/anymal_c_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/anymal_c/anymal_c_env_cfg.py new file mode 100644 index 0000000000..d5276f1404 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/anymal_c/anymal_c_env_cfg.py @@ -0,0 +1,149 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import omni.isaac.lab.envs.mdp as mdp +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import ArticulationCfg +from omni.isaac.lab.envs import DirectRLEnvCfg +from omni.isaac.lab.managers import EventTermCfg as EventTerm +from omni.isaac.lab.managers import SceneEntityCfg +from omni.isaac.lab.scene import InteractiveSceneCfg +from omni.isaac.lab.sensors import ContactSensorCfg, RayCasterCfg, patterns +from omni.isaac.lab.sim import SimulationCfg +from omni.isaac.lab.terrains import TerrainImporterCfg +from omni.isaac.lab.utils import configclass + +## +# Pre-defined configs +## +from omni.isaac.lab_assets.anymal import ANYMAL_C_CFG # isort: skip +from omni.isaac.lab.terrains.config.rough import ROUGH_TERRAINS_CFG # isort: skip + + +@configclass +class EventCfg: + """Configuration for randomization.""" + + physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*"), + "static_friction_range": (0.8, 0.8), + "dynamic_friction_range": (0.6, 0.6), + "restitution_range": (0.0, 0.0), + "num_buckets": 64, + }, + ) + + add_base_mass = EventTerm( + func=mdp.randomize_rigid_body_mass, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names="base"), + "mass_distribution_params": (-5.0, 5.0), + "operation": "add", + }, + ) + + +@configclass +class AnymalCFlatEnvCfg(DirectRLEnvCfg): + # env + episode_length_s = 20.0 + decimation = 4 + action_scale = 0.5 + action_space = 12 + observation_space = 48 + state_space = 0 + + # simulation + sim: SimulationCfg = SimulationCfg( + dt=1 / 200, + render_interval=decimation, + disable_contact_processing=True, + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="multiply", + restitution_combine_mode="multiply", + static_friction=1.0, + dynamic_friction=1.0, + restitution=0.0, + ), + ) + terrain = TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="plane", + collision_group=-1, + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="multiply", + restitution_combine_mode="multiply", + static_friction=1.0, + dynamic_friction=1.0, + restitution=0.0, + ), + debug_vis=False, + ) + + # scene + scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=4096, env_spacing=4.0, replicate_physics=True) + + # events + events: EventCfg = EventCfg() + + # robot + robot: ArticulationCfg = ANYMAL_C_CFG.replace(prim_path="/World/envs/env_.*/Robot") + contact_sensor: ContactSensorCfg = ContactSensorCfg( + prim_path="/World/envs/env_.*/Robot/.*", history_length=3, update_period=0.005, track_air_time=True + ) + + # reward scales + lin_vel_reward_scale = 1.0 + yaw_rate_reward_scale = 0.5 + z_vel_reward_scale = -2.0 + ang_vel_reward_scale = -0.05 + joint_torque_reward_scale = -2.5e-5 + joint_accel_reward_scale = -2.5e-7 + action_rate_reward_scale = -0.01 + feet_air_time_reward_scale = 0.5 + undersired_contact_reward_scale = -1.0 + flat_orientation_reward_scale = -5.0 + + +@configclass +class AnymalCRoughEnvCfg(AnymalCFlatEnvCfg): + # env + observation_space = 235 + + terrain = TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="generator", + terrain_generator=ROUGH_TERRAINS_CFG, + max_init_terrain_level=9, + collision_group=-1, + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="multiply", + restitution_combine_mode="multiply", + static_friction=1.0, + dynamic_friction=1.0, + ), + visual_material=sim_utils.MdlFileCfg( + mdl_path="{NVIDIA_NUCLEUS_DIR}/Materials/Base/Architecture/Shingles_01.mdl", + project_uvw=True, + ), + debug_vis=False, + ) + + # we add a height scanner for perceptive locomotion + height_scanner = RayCasterCfg( + prim_path="/World/envs/env_.*/Robot/base", + offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)), + attach_yaw_only=True, + pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=[1.6, 1.0]), + debug_vis=False, + mesh_prim_paths=["/World/ground"], + ) + + # reward scales (override from flat config) + flat_orientation_reward_scale = 0.0 diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cart_double_pendulum/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cart_double_pendulum/__init__.py index 2b034f4f2e..90d70311d1 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cart_double_pendulum/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cart_double_pendulum/__init__.py @@ -10,7 +10,6 @@ import gymnasium as gym from . import agents -from .cart_double_pendulum_env import CartDoublePendulumEnv, CartDoublePendulumEnvCfg ## # Register Gym environments. @@ -18,10 +17,10 @@ gym.register( id="Isaac-Cart-Double-Pendulum-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.cart_double_pendulum:CartDoublePendulumEnv", + entry_point=f"{__name__}.cart_double_pendulum_env:CartDoublePendulumEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": CartDoublePendulumEnvCfg, + "env_cfg_entry_point": f"{__name__}.cart_double_pendulum_env:CartDoublePendulumEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", "skrl_ippo_cfg_entry_point": f"{agents.__name__}:skrl_ippo_cfg.yaml", diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cart_double_pendulum/cart_double_pendulum_env.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cart_double_pendulum/cart_double_pendulum_env.py index 0b606fe899..ad8c616940 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cart_double_pendulum/cart_double_pendulum_env.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cart_double_pendulum/cart_double_pendulum_env.py @@ -27,9 +27,9 @@ class CartDoublePendulumEnvCfg(DirectMARLEnvCfg): decimation = 2 episode_length_s = 5.0 possible_agents = ["cart", "pendulum"] - num_actions = {"cart": 1, "pendulum": 1} - num_observations = {"cart": 4, "pendulum": 3} - num_states = -1 + action_spaces = {"cart": 1, "pendulum": 1} + observation_spaces = {"cart": 4, "pendulum": 3} + state_space = -1 # simulation sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/__init__.py index 9f07c14605..035cf7a7a9 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/__init__.py @@ -10,8 +10,6 @@ import gymnasium as gym from . import agents -from .cartpole_camera_env import CartpoleCameraEnv, CartpoleDepthCameraEnvCfg, CartpoleRGBCameraEnvCfg -from .cartpole_env import CartpoleEnv, CartpoleEnvCfg ## # Register Gym environments. @@ -19,10 +17,10 @@ gym.register( id="Isaac-Cartpole-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.cartpole:CartpoleEnv", + entry_point=f"{__name__}.cartpole_env:CartpoleEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": CartpoleEnvCfg, + "env_cfg_entry_point": f"{__name__}.cartpole_env:CartpoleEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CartpolePPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", @@ -32,10 +30,10 @@ gym.register( id="Isaac-Cartpole-RGB-Camera-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.cartpole:CartpoleCameraEnv", + entry_point=f"{__name__}.cartpole_camera_env:CartpoleCameraEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": CartpoleRGBCameraEnvCfg, + "env_cfg_entry_point": f"{__name__}.cartpole_camera_env:CartpoleRGBCameraEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_camera_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_camera_ppo_cfg.yaml", }, @@ -43,10 +41,10 @@ gym.register( id="Isaac-Cartpole-Depth-Camera-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.cartpole:CartpoleCameraEnv", + entry_point=f"{__name__}.cartpole_camera_env:CartpoleCameraEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": CartpoleDepthCameraEnvCfg, + "env_cfg_entry_point": f"{__name__}.cartpole_camera_env:CartpoleDepthCameraEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_camera_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_camera_ppo_cfg.yaml", }, diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_camera_env.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_camera_env.py index b2a1b1e303..e5881db086 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_camera_env.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_camera_env.py @@ -5,9 +5,7 @@ from __future__ import annotations -import gymnasium as gym import math -import numpy as np import torch from collections.abc import Sequence @@ -29,9 +27,6 @@ class CartpoleRGBCameraEnvCfg(DirectRLEnvCfg): decimation = 2 episode_length_s = 5.0 action_scale = 100.0 # [N] - num_actions = 1 - num_channels = 3 - num_states = 0 # simulation sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation) @@ -52,9 +47,13 @@ class CartpoleRGBCameraEnvCfg(DirectRLEnvCfg): width=80, height=80, ) - num_observations = num_channels * tiled_camera.height * tiled_camera.width write_image_to_file = False + # spaces + action_space = 1 + state_space = 0 + observation_space = [tiled_camera.height, tiled_camera.width, 3] + # change viewer settings viewer = ViewerCfg(eye=(20.0, 20.0, 20.0)) @@ -87,9 +86,8 @@ class CartpoleDepthCameraEnvCfg(CartpoleRGBCameraEnvCfg): height=80, ) - # env - num_channels = 1 - num_observations = num_channels * tiled_camera.height * tiled_camera.width + # spaces + observation_space = [tiled_camera.height, tiled_camera.width, 1] class CartpoleCameraEnv(DirectRLEnv): @@ -118,35 +116,6 @@ def close(self): """Cleanup for the environment.""" super().close() - def _configure_gym_env_spaces(self): - """Configure the action and observation spaces for the Gym environment.""" - # observation space (unbounded since we don't impose any limits) - self.num_actions = self.cfg.num_actions - self.num_observations = self.cfg.num_observations - self.num_states = self.cfg.num_states - - # set up spaces - self.single_observation_space = gym.spaces.Dict() - self.single_observation_space["policy"] = gym.spaces.Box( - low=-np.inf, - high=np.inf, - shape=(self.cfg.tiled_camera.height, self.cfg.tiled_camera.width, self.cfg.num_channels), - ) - if self.num_states > 0: - self.single_observation_space["critic"] = gym.spaces.Box( - low=-np.inf, - high=np.inf, - shape=(self.cfg.tiled_camera.height, self.cfg.tiled_camera.width, self.cfg.num_channels), - ) - self.single_action_space = gym.spaces.Box(low=-np.inf, high=np.inf, shape=(self.num_actions,)) - - # batch the spaces for vectorized environments - self.observation_space = gym.vector.utils.batch_space(self.single_observation_space, self.num_envs) - self.action_space = gym.vector.utils.batch_space(self.single_action_space, self.num_envs) - - # RL specifics - self.actions = torch.zeros(self.num_envs, self.num_actions, device=self.sim.device) - def _setup_scene(self): """Setup the scene with the cartpole and camera.""" self._cartpole = Articulation(self.cfg.robot_cfg) @@ -156,7 +125,7 @@ def _setup_scene(self): self.scene.clone_environments(copy_from_source=False) self.scene.filter_collisions(global_prim_paths=[]) - # add articultion and sensors to scene + # add articulation and sensors to scene self.scene.articulations["cartpole"] = self._cartpole self.scene.sensors["tiled_camera"] = self._tiled_camera # add lights diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py index 44926e95f9..1525941672 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py @@ -27,9 +27,9 @@ class CartpoleEnvCfg(DirectRLEnvCfg): decimation = 2 episode_length_s = 5.0 action_scale = 100.0 # [N] - num_actions = 1 - num_observations = 4 - num_states = 0 + action_space = 1 + observation_space = 4 + state_space = 0 # simulation sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation) @@ -74,7 +74,7 @@ def _setup_scene(self): # clone, filter, and replicate self.scene.clone_environments(copy_from_source=False) self.scene.filter_collisions(global_prim_paths=[]) - # add articultion to scene + # add articulation to scene self.scene.articulations["cartpole"] = self.cartpole # add lights light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/franka_cabinet/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/franka_cabinet/__init__.py index 5f333dc9cf..f91915d3ed 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/franka_cabinet/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/franka_cabinet/__init__.py @@ -9,7 +9,6 @@ import gymnasium as gym from . import agents -from .franka_cabinet_env import FrankaCabinetEnv, FrankaCabinetEnvCfg ## # Register Gym environments. @@ -17,10 +16,10 @@ gym.register( id="Isaac-Franka-Cabinet-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.franka_cabinet:FrankaCabinetEnv", + entry_point=f"{__name__}.franka_cabinet_env:FrankaCabinetEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": FrankaCabinetEnvCfg, + "env_cfg_entry_point": f"{__name__}.franka_cabinet_env:FrankaCabinetEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:FrankaCabinetPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/franka_cabinet/franka_cabinet_env.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/franka_cabinet/franka_cabinet_env.py index 4eb01953fe..3a6a480ed0 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/franka_cabinet/franka_cabinet_env.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/franka_cabinet/franka_cabinet_env.py @@ -28,9 +28,9 @@ class FrankaCabinetEnvCfg(DirectRLEnvCfg): # env episode_length_s = 8.3333 # 500 timesteps decimation = 2 - num_actions = 9 - num_observations = 23 - num_states = 0 + action_space = 9 + observation_space = 23 + state_space = 0 # simulation sim: SimulationCfg = SimulationCfg( diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/humanoid/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/humanoid/__init__.py index 92e8384926..9bd3dfec17 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/humanoid/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/humanoid/__init__.py @@ -10,7 +10,6 @@ import gymnasium as gym from . import agents -from .humanoid_env import HumanoidEnv, HumanoidEnvCfg ## # Register Gym environments. @@ -18,10 +17,10 @@ gym.register( id="Isaac-Humanoid-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.humanoid:HumanoidEnv", + entry_point=f"{__name__}.humanoid_env:HumanoidEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": HumanoidEnvCfg, + "env_cfg_entry_point": f"{__name__}.humanoid_env:HumanoidEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:HumanoidPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/humanoid/humanoid_env.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/humanoid/humanoid_env.py index bfaf8f8190..2a4d330e6a 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/humanoid/humanoid_env.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/humanoid/humanoid_env.py @@ -24,9 +24,9 @@ class HumanoidEnvCfg(DirectRLEnvCfg): episode_length_s = 15.0 decimation = 2 action_scale = 1.0 - num_actions = 21 - num_observations = 75 - num_states = 0 + action_space = 21 + observation_space = 75 + state_space = 0 # simulation sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/inhand_manipulation/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/inhand_manipulation/__init__.py index cdce057355..c3ee657052 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/inhand_manipulation/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/inhand_manipulation/__init__.py @@ -2,5 +2,3 @@ # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause - -from .inhand_manipulation_env import InHandManipulationEnv diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/inhand_manipulation/inhand_manipulation_env.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/inhand_manipulation/inhand_manipulation_env.py index 52f4dc6e3c..dd671d2ab9 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/inhand_manipulation/inhand_manipulation_env.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/inhand_manipulation/inhand_manipulation_env.py @@ -19,8 +19,8 @@ from omni.isaac.lab.utils.math import quat_conjugate, quat_from_angle_axis, quat_mul, sample_uniform, saturate if TYPE_CHECKING: - from omni.isaac.lab_tasks.direct.allegro_hand import AllegroHandEnvCfg - from omni.isaac.lab_tasks.direct.shadow_hand import ShadowHandEnvCfg + from omni.isaac.lab_tasks.direct.allegro_hand.allegro_hand_env_cfg import AllegroHandEnvCfg + from omni.isaac.lab_tasks.direct.shadow_hand.shadow_hand_env_cfg import ShadowHandEnvCfg class InHandManipulationEnv(DirectRLEnv): @@ -84,7 +84,7 @@ def _setup_scene(self): spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg()) # clone and replicate (no need to filter for this environment) self.scene.clone_environments(copy_from_source=False) - # add articultion to scene - we must register to scene to randomize with EventManager + # add articulation to scene - we must register to scene to randomize with EventManager self.scene.articulations["robot"] = self.hand self.scene.rigid_objects["object"] = self.object # add lights diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/locomotion/locomotion_env.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/locomotion/locomotion_env.py index c5c41a3c48..ffa572ac41 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/locomotion/locomotion_env.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/locomotion/locomotion_env.py @@ -54,7 +54,7 @@ def _setup_scene(self): # clone, filter, and replicate self.scene.clone_environments(copy_from_source=False) self.scene.filter_collisions(global_prim_paths=[self.cfg.terrain.prim_path]) - # add articultion to scene + # add articulation to scene self.scene.articulations["robot"] = self.robot # add lights light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/quadcopter/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/quadcopter/__init__.py index bc2bc74ce9..2605b599f0 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/quadcopter/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/quadcopter/__init__.py @@ -10,7 +10,6 @@ import gymnasium as gym from . import agents -from .quadcopter_env import QuadcopterEnv, QuadcopterEnvCfg ## # Register Gym environments. @@ -18,10 +17,10 @@ gym.register( id="Isaac-Quadcopter-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.quadcopter:QuadcopterEnv", + entry_point=f"{__name__}.quadcopter_env:QuadcopterEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": QuadcopterEnvCfg, + "env_cfg_entry_point": f"{__name__}.quadcopter_env:QuadcopterEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:QuadcopterPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/quadcopter/quadcopter_env.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/quadcopter/quadcopter_env.py index c6df659ec6..97156618f1 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/quadcopter/quadcopter_env.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/quadcopter/quadcopter_env.py @@ -5,6 +5,7 @@ from __future__ import annotations +import gymnasium as gym import torch import omni.isaac.lab.sim as sim_utils @@ -50,9 +51,9 @@ class QuadcopterEnvCfg(DirectRLEnvCfg): # env episode_length_s = 10.0 decimation = 2 - num_actions = 4 - num_observations = 12 - num_states = 0 + action_space = 4 + observation_space = 12 + state_space = 0 debug_vis = True ui_window_class_type = QuadcopterEnvWindow @@ -105,7 +106,7 @@ def __init__(self, cfg: QuadcopterEnvCfg, render_mode: str | None = None, **kwar super().__init__(cfg, render_mode, **kwargs) # Total thrust and moment applied to the base of the quadcopter - self._actions = torch.zeros(self.num_envs, self.cfg.num_actions, device=self.device) + self._actions = torch.zeros(self.num_envs, gym.spaces.flatdim(self.single_action_space), device=self.device) self._thrust = torch.zeros(self.num_envs, 1, 3, device=self.device) self._moment = torch.zeros(self.num_envs, 1, 3, device=self.device) # Goal position diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/__init__.py index 07ac1b10f2..73cdc43270 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/__init__.py @@ -10,19 +10,19 @@ import gymnasium as gym from . import agents -from .shadow_hand_env_cfg import ShadowHandEnvCfg, ShadowHandOpenAIEnvCfg -from .shadow_hand_vision_env import ShadowHandVisionEnvCfg, ShadowHandVisionEnvPlayCfg ## # Register Gym environments. ## +inhand_task_entry = "omni.isaac.lab_tasks.direct.inhand_manipulation" + gym.register( id="Isaac-Repose-Cube-Shadow-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.inhand_manipulation:InHandManipulationEnv", + entry_point=f"{inhand_task_entry}.inhand_manipulation_env:InHandManipulationEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": ShadowHandEnvCfg, + "env_cfg_entry_point": f"{__name__}.shadow_hand_env_cfg:ShadowHandEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", @@ -31,10 +31,10 @@ gym.register( id="Isaac-Repose-Cube-Shadow-OpenAI-FF-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.inhand_manipulation:InHandManipulationEnv", + entry_point=f"{inhand_task_entry}.inhand_manipulation_env:InHandManipulationEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": ShadowHandOpenAIEnvCfg, + "env_cfg_entry_point": f"{__name__}.shadow_hand_env_cfg:ShadowHandOpenAIEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_ff_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandAsymFFPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ff_ppo_cfg.yaml", @@ -43,10 +43,10 @@ gym.register( id="Isaac-Repose-Cube-Shadow-OpenAI-LSTM-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.inhand_manipulation:InHandManipulationEnv", + entry_point=f"{inhand_task_entry}.inhand_manipulation_env:InHandManipulationEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": ShadowHandOpenAIEnvCfg, + "env_cfg_entry_point": f"{__name__}.shadow_hand_env_cfg:ShadowHandOpenAIEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_lstm_cfg.yaml", }, ) @@ -55,10 +55,10 @@ gym.register( id="Isaac-Repose-Cube-Shadow-Vision-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.shadow_hand.shadow_hand_vision_env:ShadowHandVisionEnv", + entry_point=f"{__name__}.shadow_hand_vision_env:ShadowHandVisionEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": ShadowHandVisionEnvCfg, + "env_cfg_entry_point": f"{__name__}.shadow_hand_vision_env:ShadowHandVisionEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandVisionFFPPORunnerCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_vision_cfg.yaml", }, @@ -66,10 +66,10 @@ gym.register( id="Isaac-Repose-Cube-Shadow-Vision-Direct-Play-v0", - entry_point="omni.isaac.lab_tasks.direct.shadow_hand.shadow_hand_vision_env:ShadowHandVisionEnv", + entry_point=f"{__name__}.shadow_hand_vision_env:ShadowHandVisionEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": ShadowHandVisionEnvPlayCfg, + "env_cfg_entry_point": f"{__name__}.shadow_hand_vision_env:ShadowHandVisionEnvPlayCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandVisionFFPPORunnerCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_vision_cfg.yaml", }, diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/feature_extractor.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/feature_extractor.py index 1dbfb39b1a..fc92bbfb1b 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/feature_extractor.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/feature_extractor.py @@ -7,7 +7,6 @@ import os import torch import torch.nn as nn - import torchvision from omni.isaac.lab.sensors import save_images_to_file diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/shadow_hand_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/shadow_hand_env_cfg.py index f4b8407296..af88124792 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/shadow_hand_env_cfg.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/shadow_hand_env_cfg.py @@ -119,9 +119,9 @@ class ShadowHandEnvCfg(DirectRLEnvCfg): # env decimation = 2 episode_length_s = 10.0 - num_actions = 20 - num_observations = 157 # (full) - num_states = 0 + action_space = 20 + observation_space = 157 # (full) + state_space = 0 asymmetric_obs = False obs_type = "full" @@ -232,9 +232,9 @@ class ShadowHandOpenAIEnvCfg(ShadowHandEnvCfg): # env decimation = 3 episode_length_s = 8.0 - num_actions = 20 - num_observations = 42 - num_states = 187 + action_space = 20 + observation_space = 42 + state_space = 187 asymmetric_obs = True obs_type = "openai" # simulation diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/shadow_hand_vision_env.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/shadow_hand_vision_env.py index b025bfb052..8677c16eba 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/shadow_hand_vision_env.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand/shadow_hand_vision_env.py @@ -48,8 +48,8 @@ class ShadowHandVisionEnvCfg(ShadowHandEnvCfg): feature_extractor = FeatureExtractorCfg() # env - num_observations = 164 + 27 # state observation + vision CNN embedding - num_states = 187 + 27 # asymettric states + vision CNN embedding + observation_space = 164 + 27 # state observation + vision CNN embedding + state_space = 187 + 27 # asymettric states + vision CNN embedding @configclass @@ -88,7 +88,7 @@ def _setup_scene(self): sem.GetSemanticDataAttr().Set("cube") # clone and replicate (no need to filter for this environment) self.scene.clone_environments(copy_from_source=False) - # add articultion to scene - we must register to scene to randomize with EventManager + # add articulation to scene - we must register to scene to randomize with EventManager self.scene.articulations["robot"] = self.hand self.scene.rigid_objects["object"] = self.object self.scene.sensors["tiled_camera"] = self._tiled_camera diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand_over/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand_over/__init__.py index 0beb0a0d61..8eef9dd395 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand_over/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand_over/__init__.py @@ -10,8 +10,6 @@ import gymnasium as gym from . import agents -from .shadow_hand_over_env import ShadowHandOverEnv -from .shadow_hand_over_env_cfg import ShadowHandOverEnvCfg ## # Register Gym environments. @@ -19,10 +17,10 @@ gym.register( id="Isaac-Shadow-Hand-Over-Direct-v0", - entry_point="omni.isaac.lab_tasks.direct.shadow_hand_over:ShadowHandOverEnv", + entry_point=f"{__name__}.shadow_hand_over_env:ShadowHandOverEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": ShadowHandOverEnvCfg, + "env_cfg_entry_point": f"{__name__}.shadow_hand_over_env_cfg:ShadowHandOverEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", "skrl_ippo_cfg_entry_point": f"{agents.__name__}:skrl_ippo_cfg.yaml", diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand_over/shadow_hand_over_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand_over/shadow_hand_over_env_cfg.py index d6dbb3d6a2..d3a7c33b3f 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand_over/shadow_hand_over_env_cfg.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/shadow_hand_over/shadow_hand_over_env_cfg.py @@ -118,9 +118,9 @@ class ShadowHandOverEnvCfg(DirectMARLEnvCfg): decimation = 2 episode_length_s = 7.5 possible_agents = ["right_hand", "left_hand"] - num_actions = {"right_hand": 20, "left_hand": 20} - num_observations = {"right_hand": 157, "left_hand": 157} - num_states = 290 + action_spaces = {"right_hand": 20, "left_hand": 20} + observation_spaces = {"right_hand": 157, "left_hand": 157} + state_space = 290 # simulation sim: SimulationCfg = SimulationCfg( diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/ant/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/ant/__init__.py index ed3c86eab3..a128cf5116 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/ant/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/ant/__init__.py @@ -9,7 +9,7 @@ import gymnasium as gym -from . import agents, ant_env_cfg +from . import agents ## # Register Gym environments. @@ -20,7 +20,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": ant_env_cfg.AntEnvCfg, + "env_cfg_entry_point": f"{__name__}.ant_env_cfg:AntEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AntPPORunnerCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", diff --git a/source/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/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/__init__.py index 7a3070d775..3b0107ecb7 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/__init__.py @@ -10,8 +10,6 @@ import gymnasium as gym from . import agents -from .cartpole_camera_env_cfg import CartpoleDepthCameraEnvCfg, CartpoleRGBCameraEnvCfg -from .cartpole_env_cfg import CartpoleEnvCfg ## # Register Gym environments. @@ -22,7 +20,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": CartpoleEnvCfg, + "env_cfg_entry_point": f"{__name__}.cartpole_env_cfg:CartpoleEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CartpolePPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", @@ -31,21 +29,41 @@ ) gym.register( - id="Isaac-Cartpole-RGB-Camera-v0", + id="Isaac-Cartpole-RGB-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": CartpoleRGBCameraEnvCfg, + "env_cfg_entry_point": f"{__name__}.cartpole_camera_env_cfg:CartpoleRGBCameraEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_camera_ppo_cfg.yaml", }, ) gym.register( - id="Isaac-Cartpole-Depth-Camera-v0", + id="Isaac-Cartpole-Depth-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": CartpoleDepthCameraEnvCfg, + "env_cfg_entry_point": f"{__name__}.cartpole_camera_env_cfg:CartpoleDepthCameraEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_camera_ppo_cfg.yaml", }, ) + +gym.register( + id="Isaac-Cartpole-RGB-ResNet18-v0", + entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.cartpole_camera_env_cfg:CartpoleResNet18CameraEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_feature_ppo_cfg.yaml", + }, +) + +gym.register( + id="Isaac-Cartpole-RGB-TheiaTiny-v0", + entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.cartpole_camera_env_cfg:CartpoleTheiaTinyCameraEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_feature_ppo_cfg.yaml", + }, +) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/agents/rl_games_feature_ppo_cfg.yaml b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/agents/rl_games_feature_ppo_cfg.yaml new file mode 100644 index 0000000000..41e265e9f2 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/agents/rl_games_feature_ppo_cfg.yaml @@ -0,0 +1,79 @@ +params: + seed: 42 + + # environment wrapper clipping + env: + # added to the wrapper + clip_observations: 5.0 + # can make custom wrapper? + clip_actions: 1.0 + + algo: + name: a2c_continuous + + model: + name: continuous_a2c_logstd + + # doesn't have this fine grained control but made it close + network: + name: actor_critic + separate: False + space: + continuous: + mu_activation: None + sigma_activation: None + + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: True + mlp: + units: [256] + activation: elu + d2rl: False + + initializer: + name: default + regularizer: + name: None + + load_checkpoint: False # flag which sets whether to load the checkpoint + load_path: '' # path to the checkpoint to load + + config: + name: cartpole_features + env_name: rlgpu + device: 'cuda:0' + device_name: 'cuda:0' + multi_gpu: False + ppo: True + mixed_precision: False + normalize_input: True + normalize_value: True + value_bootstraop: True + num_actors: -1 # configured from the script (based on num_envs) + reward_shaper: + scale_value: 1.0 + normalize_advantage: True + gamma: 0.99 + tau : 0.95 + learning_rate: 3e-4 + lr_schedule: adaptive + kl_threshold: 0.008 + score_to_win: 20000 + max_epochs: 200 + save_best_after: 50 + save_frequency: 25 + grad_norm: 1.0 + entropy_coef: 0.0 + truncate_grads: True + e_clip: 0.2 + horizon_length: 16 + minibatch_size: 2048 + mini_epochs: 8 + critic_coef: 4 + clip_value: True + seq_length: 4 + bounds_loss_coef: 0.0001 diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py index ce5a6c90b8..35c828d2cb 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py @@ -78,7 +78,7 @@ class DepthObservationsCfg: """Observation specifications for the MDP.""" @configclass - class DepthCameraPolicyCfg(RGBObservationsCfg.RGBCameraPolicyCfg): + class DepthCameraPolicyCfg(ObsGroup): """Observations for policy group with depth images.""" image = ObsTerm( @@ -88,6 +88,43 @@ class DepthCameraPolicyCfg(RGBObservationsCfg.RGBCameraPolicyCfg): policy: ObsGroup = DepthCameraPolicyCfg() +@configclass +class ResNet18ObservationCfg: + """Observation specifications for the MDP.""" + + @configclass + class ResNet18FeaturesCameraPolicyCfg(ObsGroup): + """Observations for policy group with features extracted from RGB images with a frozen ResNet18.""" + + image = ObsTerm( + func=mdp.image_features, + params={"sensor_cfg": SceneEntityCfg("tiled_camera"), "data_type": "rgb", "model_name": "resnet18"}, + ) + + policy: ObsGroup = ResNet18FeaturesCameraPolicyCfg() + + +@configclass +class TheiaTinyObservationCfg: + """Observation specifications for the MDP.""" + + @configclass + class TheiaTinyFeaturesCameraPolicyCfg(ObsGroup): + """Observations for policy group with features extracted from RGB images with a frozen Theia-Tiny Transformer""" + + image = ObsTerm( + func=mdp.image_features, + params={ + "sensor_cfg": SceneEntityCfg("tiled_camera"), + "data_type": "rgb", + "model_name": "theia-tiny-patch16-224-cddsv", + "model_device": "cuda:0", + }, + ) + + policy: ObsGroup = TheiaTinyFeaturesCameraPolicyCfg() + + ## # Environment configuration ## @@ -97,13 +134,43 @@ class DepthCameraPolicyCfg(RGBObservationsCfg.RGBCameraPolicyCfg): class CartpoleRGBCameraEnvCfg(CartpoleEnvCfg): """Configuration for the cartpole environment with RGB camera.""" - scene: CartpoleSceneCfg = CartpoleRGBCameraSceneCfg(num_envs=1024, env_spacing=20) + scene: CartpoleRGBCameraSceneCfg = CartpoleRGBCameraSceneCfg(num_envs=1024, env_spacing=20) observations: RGBObservationsCfg = RGBObservationsCfg() + def __post_init__(self): + super().__post_init__() + # remove ground as it obstructs the camera + self.scene.ground = None + # viewer settings + self.viewer.eye = (7.0, 0.0, 2.5) + self.viewer.lookat = (0.0, 0.0, 2.5) + @configclass class CartpoleDepthCameraEnvCfg(CartpoleEnvCfg): """Configuration for the cartpole environment with depth camera.""" - scene: CartpoleSceneCfg = CartpoleDepthCameraSceneCfg(num_envs=1024, env_spacing=20) + scene: CartpoleDepthCameraSceneCfg = CartpoleDepthCameraSceneCfg(num_envs=1024, env_spacing=20) observations: DepthObservationsCfg = DepthObservationsCfg() + + def __post_init__(self): + super().__post_init__() + # remove ground as it obstructs the camera + self.scene.ground = None + # viewer settings + self.viewer.eye = (7.0, 0.0, 2.5) + self.viewer.lookat = (0.0, 0.0, 2.5) + + +@configclass +class CartpoleResNet18CameraEnvCfg(CartpoleRGBCameraEnvCfg): + """Configuration for the cartpole environment with ResNet18 features as observations.""" + + observations: ResNet18ObservationCfg = ResNet18ObservationCfg() + + +@configclass +class CartpoleTheiaTinyCameraEnvCfg(CartpoleRGBCameraEnvCfg): + """Configuration for the cartpole environment with Theia-Tiny features as observations.""" + + observations: TheiaTinyObservationCfg = TheiaTinyObservationCfg() 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..ca8784d8db 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 @@ -48,11 +48,6 @@ class CartpoleSceneCfg(InteractiveSceneCfg): prim_path="/World/DomeLight", spawn=sim_utils.DomeLightCfg(color=(0.9, 0.9, 0.9), intensity=500.0), ) - distant_light = AssetBaseCfg( - prim_path="/World/DistantLight", - spawn=sim_utils.DistantLightCfg(color=(0.9, 0.9, 0.9), intensity=2500.0), - init_state=AssetBaseCfg.InitialStateCfg(rot=(0.738, 0.477, 0.477, 0.0)), - ) ## @@ -60,14 +55,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 +149,6 @@ class TerminationsCfg: ) -@configclass -class CurriculumCfg: - """Configuration for the curriculum.""" - - pass - - ## # Environment configuration ## @@ -185,11 +165,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/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/humanoid/__init__.py index c902b83af5..497901f711 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/humanoid/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/humanoid/__init__.py @@ -9,7 +9,7 @@ import gymnasium as gym -from . import agents, humanoid_env_cfg +from . import agents ## # Register Gym environments. @@ -20,7 +20,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": humanoid_env_cfg.HumanoidEnvCfg, + "env_cfg_entry_point": f"{__name__}.humanoid_env_cfg:HumanoidEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:HumanoidPPORunnerCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", diff --git a/source/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/a1/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/a1/__init__.py index 80e9c3750f..fd628c91f5 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/a1/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/a1/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym -from . import agents, flat_env_cfg, rough_env_cfg +from . import agents ## # Register Gym environments. @@ -16,7 +16,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.UnitreeA1FlatEnvCfg, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:UnitreeA1FlatEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UnitreeA1FlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -27,7 +27,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.UnitreeA1FlatEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:UnitreeA1FlatEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UnitreeA1FlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -38,7 +38,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.UnitreeA1RoughEnvCfg, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:UnitreeA1RoughEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UnitreeA1RoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, @@ -49,7 +49,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.UnitreeA1RoughEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:UnitreeA1RoughEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UnitreeA1RoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/anymal_b/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/anymal_b/__init__.py index 65e89a1d9e..3796c67d81 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/anymal_b/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/anymal_b/__init__.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: BSD-3-Clause import gymnasium as gym -from . import agents, flat_env_cfg, rough_env_cfg +from . import agents ## # Register Gym environments. @@ -15,7 +15,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.AnymalBFlatEnvCfg, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:AnymalBFlatEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalBFlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -26,7 +26,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.AnymalBFlatEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:AnymalBFlatEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalBFlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -37,7 +37,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.AnymalBRoughEnvCfg, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:AnymalBRoughEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalBRoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, @@ -48,7 +48,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.AnymalBRoughEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:AnymalBRoughEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalBRoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py index 1d5aa41c87..770b52337e 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym -from . import agents, flat_env_cfg, rough_env_cfg +from . import agents ## # Register Gym environments. @@ -16,7 +16,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.AnymalCFlatEnvCfg, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:AnymalCFlatEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalCFlatPPORunnerCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_flat_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", @@ -28,7 +28,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.AnymalCFlatEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:AnymalCFlatEnvCfg_PLAY", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_flat_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalCFlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", @@ -40,7 +40,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.AnymalCRoughEnvCfg, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:AnymalCRoughEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_rough_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalCRoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", @@ -52,7 +52,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.AnymalCRoughEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:AnymalCRoughEnvCfg_PLAY", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_rough_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalCRoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/anymal_d/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/anymal_d/__init__.py index 0f6b8a4776..eea14799e9 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/anymal_d/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/anymal_d/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym -from . import agents, flat_env_cfg, rough_env_cfg +from . import agents ## # Register Gym environments. @@ -16,7 +16,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.AnymalDFlatEnvCfg, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:AnymalDFlatEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalDFlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -27,7 +27,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.AnymalDFlatEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:AnymalDFlatEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalDFlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -38,7 +38,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.AnymalDRoughEnvCfg, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:AnymalDRoughEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalDRoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, @@ -49,7 +49,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.AnymalDRoughEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:AnymalDRoughEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AnymalDRoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/cassie/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/cassie/__init__.py index 495491f617..69658caf46 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/cassie/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/cassie/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym -from . import agents, flat_env_cfg, rough_env_cfg +from . import agents ## # Register Gym environments. @@ -16,7 +16,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.CassieFlatEnvCfg, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:CassieFlatEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CassieFlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -27,7 +27,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.CassieFlatEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:CassieFlatEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CassieFlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -38,7 +38,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.CassieRoughEnvCfg, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:CassieRoughEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CassieRoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, @@ -49,7 +49,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.CassieRoughEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:CassieRoughEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CassieRoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/g1/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/g1/__init__.py index c84c627099..316252f77d 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/g1/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/g1/__init__.py @@ -5,19 +5,18 @@ import gymnasium as gym -from . import agents, flat_env_cfg, rough_env_cfg +from . import agents ## # Register Gym environments. ## - gym.register( id="Isaac-Velocity-Rough-G1-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.G1RoughEnvCfg, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:G1RoughEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:G1RoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, @@ -29,7 +28,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.G1RoughEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:G1RoughEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:G1RoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, @@ -41,7 +40,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.G1FlatEnvCfg, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:G1FlatEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:G1FlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -53,7 +52,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.G1FlatEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:G1FlatEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:G1FlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/g1/rough_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/g1/rough_env_cfg.py index 98503f4fb8..0be5d6eba8 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/g1/rough_env_cfg.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/g1/rough_env_cfg.py @@ -5,7 +5,6 @@ from omni.isaac.lab.managers import RewardTermCfg as RewTerm from omni.isaac.lab.managers import SceneEntityCfg -from omni.isaac.lab.managers import TerminationTermCfg as DoneTerm from omni.isaac.lab.utils import configclass import omni.isaac.lab_tasks.manager_based.locomotion.velocity.mdp as mdp @@ -104,21 +103,9 @@ class G1Rewards(RewardsCfg): ) -@configclass -class TerminationsCfg: - """Termination terms for the MDP.""" - - time_out = DoneTerm(func=mdp.time_out, time_out=True) - base_contact = DoneTerm( - func=mdp.illegal_contact, - params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names="torso_link"), "threshold": 1.0}, - ) - - @configclass class G1RoughEnvCfg(LocomotionVelocityRoughEnvCfg): rewards: G1Rewards = G1Rewards() - terminations: TerminationsCfg = TerminationsCfg() def __post_init__(self): # post init of parent @@ -163,6 +150,9 @@ def __post_init__(self): self.commands.base_velocity.ranges.lin_vel_y = (-0.0, 0.0) self.commands.base_velocity.ranges.ang_vel_z = (-1.0, 1.0) + # terminations + self.terminations.base_contact.params["sensor_cfg"].body_names = "torso_link" + @configclass class G1RoughEnvCfg_PLAY(G1RoughEnvCfg): diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/go1/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/go1/__init__.py index 260e68cff1..b30722bc7f 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/go1/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/go1/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym -from . import agents, flat_env_cfg, rough_env_cfg +from . import agents ## # Register Gym environments. @@ -16,7 +16,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.UnitreeGo1FlatEnvCfg, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:UnitreeGo1FlatEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UnitreeGo1FlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -27,7 +27,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.UnitreeGo1FlatEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:UnitreeGo1FlatEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UnitreeGo1FlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -38,7 +38,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.UnitreeGo1RoughEnvCfg, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:UnitreeGo1RoughEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UnitreeGo1RoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, @@ -49,7 +49,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.UnitreeGo1RoughEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:UnitreeGo1RoughEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UnitreeGo1RoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/go2/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/go2/__init__.py index 27b3483956..85071ad547 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/go2/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/go2/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym -from . import agents, flat_env_cfg, rough_env_cfg +from . import agents ## # Register Gym environments. @@ -16,7 +16,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.UnitreeGo2FlatEnvCfg, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:UnitreeGo2FlatEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UnitreeGo2FlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -27,7 +27,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.UnitreeGo2FlatEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:UnitreeGo2FlatEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UnitreeGo2FlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -38,7 +38,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.UnitreeGo2RoughEnvCfg, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:UnitreeGo2RoughEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UnitreeGo2RoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, @@ -49,7 +49,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.UnitreeGo2RoughEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:UnitreeGo2RoughEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UnitreeGo2RoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/h1/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/h1/__init__.py index 5c405ea4ef..b03937ae60 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/h1/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/h1/__init__.py @@ -5,19 +5,18 @@ import gymnasium as gym -from . import agents, flat_env_cfg, rough_env_cfg +from . import agents ## # Register Gym environments. ## - gym.register( id="Isaac-Velocity-Rough-H1-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.H1RoughEnvCfg, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:H1RoughEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:H1RoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, @@ -29,7 +28,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": rough_env_cfg.H1RoughEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.rough_env_cfg:H1RoughEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:H1RoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", }, @@ -41,7 +40,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.H1FlatEnvCfg, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:H1FlatEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:H1FlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -53,7 +52,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.H1FlatEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:H1FlatEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:H1FlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/h1/rough_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/h1/rough_env_cfg.py index 516119908f..e1414cb364 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/h1/rough_env_cfg.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/h1/rough_env_cfg.py @@ -5,7 +5,6 @@ from omni.isaac.lab.managers import RewardTermCfg as RewTerm from omni.isaac.lab.managers import SceneEntityCfg -from omni.isaac.lab.managers import TerminationTermCfg as DoneTerm from omni.isaac.lab.utils import configclass import omni.isaac.lab_tasks.manager_based.locomotion.velocity.mdp as mdp @@ -71,21 +70,9 @@ class H1Rewards(RewardsCfg): ) -@configclass -class TerminationsCfg: - """Termination terms for the MDP.""" - - time_out = DoneTerm(func=mdp.time_out, time_out=True) - base_contact = DoneTerm( - func=mdp.illegal_contact, - params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names=".*torso_link"), "threshold": 1.0}, - ) - - @configclass class H1RoughEnvCfg(LocomotionVelocityRoughEnvCfg): rewards: H1Rewards = H1Rewards() - terminations: TerminationsCfg = TerminationsCfg() def __post_init__(self): # post init of parent @@ -127,6 +114,9 @@ def __post_init__(self): self.commands.base_velocity.ranges.lin_vel_y = (0.0, 0.0) self.commands.base_velocity.ranges.ang_vel_z = (-1.0, 1.0) + # terminations + self.terminations.base_contact.params["sensor_cfg"].body_names = ".*torso_link" + @configclass class H1RoughEnvCfg_PLAY(H1RoughEnvCfg): diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/spot/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/spot/__init__.py index fda9123ea6..a2a852dd68 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/spot/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/config/spot/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym -from . import agents, flat_env_cfg +from . import agents ## # Register Gym environments. @@ -16,7 +16,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.SpotFlatEnvCfg, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:SpotFlatEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:SpotFlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -27,7 +27,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.SpotFlatEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:SpotFlatEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:SpotFlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, 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/locomotion/velocity/mdp/rewards.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/mdp/rewards.py index db8ad9a0cc..08c1df0097 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/mdp/rewards.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/mdp/rewards.py @@ -77,7 +77,7 @@ def feet_slide(env, sensor_cfg: SceneEntityCfg, asset_cfg: SceneEntityCfg = Scen contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] contacts = contact_sensor.data.net_forces_w_history[:, :, sensor_cfg.body_ids, :].norm(dim=-1).max(dim=1)[0] > 1.0 asset = env.scene[asset_cfg.name] - body_vel = asset.data.body_lin_vel_w[:, asset_cfg.body_ids, :2] + body_vel = asset.data.body_lin_vel_w[:, sensor_cfg.body_ids, :2] reward = torch.sum(body_vel.norm(dim=-1) * contacts, dim=1) return reward 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/manipulation/cabinet/config/franka/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/cabinet/config/franka/__init__.py index 192bcf6d6b..ec861b8cd5 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/cabinet/config/franka/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/cabinet/config/franka/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym -from . import agents, ik_abs_env_cfg, ik_rel_env_cfg, joint_pos_env_cfg +from . import agents ## # Register Gym environments. @@ -19,7 +19,7 @@ id="Isaac-Open-Drawer-Franka-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": joint_pos_env_cfg.FrankaCabinetEnvCfg, + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:FrankaCabinetEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CabinetPPORunnerCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", @@ -31,7 +31,7 @@ id="Isaac-Open-Drawer-Franka-Play-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": joint_pos_env_cfg.FrankaCabinetEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:FrankaCabinetEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CabinetPPORunnerCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", @@ -48,7 +48,7 @@ id="Isaac-Open-Drawer-Franka-IK-Abs-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": ik_abs_env_cfg.FrankaCabinetEnvCfg, + "env_cfg_entry_point": f"{__name__}.ik_abs_env_cfg:FrankaCabinetEnvCfg", }, disable_env_checker=True, ) @@ -61,7 +61,7 @@ id="Isaac-Open-Drawer-Franka-IK-Rel-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": ik_rel_env_cfg.FrankaCabinetEnvCfg, + "env_cfg_entry_point": f"{__name__}.ik_rel_env_cfg:FrankaCabinetEnvCfg", }, disable_env_checker=True, ) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/inhand/config/allegro_hand/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/inhand/config/allegro_hand/__init__.py index e1d162515d..031a267384 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/inhand/config/allegro_hand/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/inhand/config/allegro_hand/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym -from . import agents, allegro_env_cfg +from . import agents ## # Register Gym environments. @@ -20,7 +20,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": allegro_env_cfg.AllegroCubeEnvCfg, + "env_cfg_entry_point": f"{__name__}.allegro_env_cfg:AllegroCubeEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AllegroCubePPORunnerCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", @@ -32,7 +32,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": allegro_env_cfg.AllegroCubeEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.allegro_env_cfg:AllegroCubeEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AllegroCubePPORunnerCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", @@ -48,7 +48,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": allegro_env_cfg.AllegroCubeNoVelObsEnvCfg, + "env_cfg_entry_point": f"{__name__}.allegro_env_cfg:AllegroCubeNoVelObsEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AllegroCubeNoVelObsPPORunnerCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", @@ -60,7 +60,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": allegro_env_cfg.AllegroCubeNoVelObsEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.allegro_env_cfg:AllegroCubeNoVelObsEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AllegroCubeNoVelObsPPORunnerCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/lift/config/franka/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/lift/config/franka/__init__.py index 14657e4f92..644ac056e9 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/lift/config/franka/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/lift/config/franka/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym import os -from . import agents, ik_abs_env_cfg, ik_rel_env_cfg, joint_pos_env_cfg +from . import agents ## # Register Gym environments. @@ -19,10 +19,11 @@ id="Isaac-Lift-Cube-Franka-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": joint_pos_env_cfg.FrankaCubeLiftEnvCfg, + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:FrankaCubeLiftEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:LiftCubePPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "sb3_cfg_entry_point": f"{agents.__name__}:sb3_ppo_cfg.yaml", }, disable_env_checker=True, ) @@ -31,10 +32,11 @@ id="Isaac-Lift-Cube-Franka-Play-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": joint_pos_env_cfg.FrankaCubeLiftEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:FrankaCubeLiftEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:LiftCubePPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "sb3_cfg_entry_point": f"{agents.__name__}:sb3_ppo_cfg.yaml", }, disable_env_checker=True, ) @@ -47,7 +49,7 @@ id="Isaac-Lift-Cube-Franka-IK-Abs-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": ik_abs_env_cfg.FrankaCubeLiftEnvCfg, + "env_cfg_entry_point": f"{__name__}.ik_abs_env_cfg:FrankaCubeLiftEnvCfg", }, disable_env_checker=True, ) @@ -56,7 +58,7 @@ id="Isaac-Lift-Teddy-Bear-Franka-IK-Abs-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": ik_abs_env_cfg.FrankaTeddyBearLiftEnvCfg, + "env_cfg_entry_point": f"{__name__}.ik_abs_env_cfg:FrankaTeddyBearLiftEnvCfg", }, disable_env_checker=True, ) @@ -69,7 +71,7 @@ id="Isaac-Lift-Cube-Franka-IK-Rel-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": ik_rel_env_cfg.FrankaCubeLiftEnvCfg, + "env_cfg_entry_point": f"{__name__}.ik_rel_env_cfg:FrankaCubeLiftEnvCfg", "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc.json"), }, disable_env_checker=True, diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/lift/config/franka/agents/sb3_ppo_cfg.yaml b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/lift/config/franka/agents/sb3_ppo_cfg.yaml index 57960b8822..6d6f15781a 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/lift/config/franka/agents/sb3_ppo_cfg.yaml +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/lift/config/franka/agents/sb3_ppo_cfg.yaml @@ -16,7 +16,7 @@ learning_rate: !!float 3e-4 clip_range: 0.2 policy_kwargs: "dict( activation_fn=nn.ELU, - net_arch=[32, 32, dict(pi=[256, 128, 64], vf=[256, 128, 64])] + net_arch=dict(pi=[256, 128, 64], vf=[256, 128, 64]) )" target_kl: 0.01 max_grad_norm: 1.0 diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/reach/config/franka/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/reach/config/franka/__init__.py index 66146302b2..f66d0ba910 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/reach/config/franka/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/reach/config/franka/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym -from . import agents, ik_abs_env_cfg, ik_rel_env_cfg, joint_pos_env_cfg +from . import agents ## # Register Gym environments. @@ -20,7 +20,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": joint_pos_env_cfg.FrankaReachEnvCfg, + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:FrankaReachEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:FrankaReachPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", @@ -32,7 +32,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": joint_pos_env_cfg.FrankaReachEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:FrankaReachEnvCfg_PLAY", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:FrankaReachPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", @@ -48,7 +48,7 @@ id="Isaac-Reach-Franka-IK-Abs-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": ik_abs_env_cfg.FrankaReachEnvCfg, + "env_cfg_entry_point": f"{__name__}.ik_abs_env_cfg:FrankaReachEnvCfg", }, disable_env_checker=True, ) @@ -61,7 +61,7 @@ id="Isaac-Reach-Franka-IK-Rel-v0", entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": ik_rel_env_cfg.FrankaReachEnvCfg, + "env_cfg_entry_point": f"{__name__}.ik_rel_env_cfg:FrankaReachEnvCfg", }, disable_env_checker=True, ) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/reach/config/ur_10/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/reach/config/ur_10/__init__.py index ca788c59d9..c2ce4c673d 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/reach/config/ur_10/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/reach/config/ur_10/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym -from . import agents, joint_pos_env_cfg +from . import agents ## # Register Gym environments. @@ -16,7 +16,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": joint_pos_env_cfg.UR10ReachEnvCfg, + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:UR10ReachEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UR10ReachPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", @@ -28,7 +28,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": joint_pos_env_cfg.UR10ReachEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:UR10ReachEnvCfg_PLAY", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UR10ReachPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/__init__.py new file mode 100644 index 0000000000..a6be077a39 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configurations for the object stack environments.""" + +# We leave this file empty since we don't want to expose any configs in this package directly. +# We still need this file to import the "config" module in the parent package. diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/__init__.py new file mode 100644 index 0000000000..a6be077a39 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configurations for the object stack environments.""" + +# We leave this file empty since we don't want to expose any configs in this package directly. +# We still need this file to import the "config" module in the parent package. diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/__init__.py new file mode 100644 index 0000000000..5a564fc996 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/__init__.py @@ -0,0 +1,64 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +import gymnasium as gym +import os + +from . import ( + agents, + stack_ik_rel_env_cfg, + stack_ik_rel_instance_randomize_env_cfg, + stack_joint_pos_env_cfg, + stack_joint_pos_instance_randomize_env_cfg, +) + +## +# Register Gym environments. +## + +## +# Joint Position Control +## + +gym.register( + id="Isaac-Stack-Cube-Franka-v0", + entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", + kwargs={ + "env_cfg_entry_point": stack_joint_pos_env_cfg.FrankaCubeStackEnvCfg, + }, + disable_env_checker=True, +) + +gym.register( + id="Isaac-Stack-Cube-Instance-Randomize-Franka-v0", + entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", + kwargs={ + "env_cfg_entry_point": stack_joint_pos_instance_randomize_env_cfg.FrankaCubeStackInstanceRandomizeEnvCfg, + }, + disable_env_checker=True, +) + + +## +# Inverse Kinematics - Relative Pose Control +## + +gym.register( + id="Isaac-Stack-Cube-Franka-IK-Rel-v0", + entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", + kwargs={ + "env_cfg_entry_point": stack_ik_rel_env_cfg.FrankaCubeStackEnvCfg, + "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc_rnn_low_dim.json"), + }, + disable_env_checker=True, +) + +gym.register( + id="Isaac-Stack-Cube-Instance-Randomize-Franka-IK-Rel-v0", + entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", + kwargs={ + "env_cfg_entry_point": stack_ik_rel_instance_randomize_env_cfg.FrankaCubeStackInstanceRandomizeEnvCfg, + }, + disable_env_checker=True, +) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/agents/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/agents/__init__.py new file mode 100644 index 0000000000..c3ee657052 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/agents/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/agents/robomimic/bc_rnn_low_dim.json b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/agents/robomimic/bc_rnn_low_dim.json new file mode 100644 index 0000000000..24d807ac34 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/agents/robomimic/bc_rnn_low_dim.json @@ -0,0 +1,101 @@ +{ + "algo_name": "bc", + "experiment": { + "name": "bc", + "validate": false, + "logging": { + "terminal_output_to_txt": true, + "log_tb": true + }, + "save": { + "enabled": true, + "every_n_seconds": null, + "every_n_epochs": 100, + "epochs": [], + "on_best_validation": false, + "on_best_rollout_return": false, + "on_best_rollout_success_rate": true + }, + "epoch_every_n_steps": 100, + "env": null, + "additional_envs": null, + "render": false, + "render_video": false, + "rollout": { + "enabled": false + } + }, + "train": { + "data": null, + "num_data_workers": 4, + "hdf5_cache_mode": "all", + "hdf5_use_swmr": true, + "hdf5_normalize_obs": false, + "hdf5_filter_key": null, + "hdf5_validation_filter_key": null, + "seq_length": 10, + "dataset_keys": [ + "actions" + ], + "goal_mode": null, + "cuda": true, + "batch_size": 100, + "num_epochs": 2000, + "seed": 101 + }, + "algo": { + "optim_params": { + "policy": { + "optimizer_type": "adam", + "learning_rate": { + "initial": 0.001, + "decay_factor": 0.1, + "epoch_schedule": [], + "scheduler_type": "multistep" + }, + "regularization": { + "L2": 0.0 + } + } + }, + "loss": { + "l2_weight": 1.0, + "l1_weight": 0.0, + "cos_weight": 0.0 + }, + "actor_layer_dims": [], + "gmm": { + "enabled": true, + "num_modes": 5, + "min_std": 0.0001, + "std_activation": "softplus", + "low_noise_eval": true + }, + "rnn": { + "enabled": true, + "horizon": 10, + "hidden_dim": 400, + "rnn_type": "LSTM", + "num_layers": 2, + "open_loop": false, + "kwargs": { + "bidirectional": false + } + } + }, + "observation": { + "modalities": { + "obs": { + "low_dim": [ + "eef_pos", + "eef_quat", + "gripper_pos", + "object" + ], + "rgb": [], + "depth": [], + "scan": [] + } + } + } +} diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_env_cfg.py new file mode 100644 index 0000000000..f95f1c61dc --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_env_cfg.py @@ -0,0 +1,36 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from omni.isaac.lab.controllers.differential_ik_cfg import DifferentialIKControllerCfg +from omni.isaac.lab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg +from omni.isaac.lab.utils import configclass + +from . import stack_joint_pos_env_cfg + +## +# Pre-defined configs +## +from omni.isaac.lab_assets.franka import FRANKA_PANDA_HIGH_PD_CFG # isort: skip + + +@configclass +class FrankaCubeStackEnvCfg(stack_joint_pos_env_cfg.FrankaCubeStackEnvCfg): + def __post_init__(self): + # post init of parent + super().__post_init__() + + # Set Franka as robot + # We switch here to a stiffer PD controller for IK tracking to be better. + self.scene.robot = FRANKA_PANDA_HIGH_PD_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # Set actions for the specific robot type (franka) + self.actions.arm_action = DifferentialInverseKinematicsActionCfg( + asset_name="robot", + joint_names=["panda_joint.*"], + body_name="panda_hand", + controller=DifferentialIKControllerCfg(command_type="pose", use_relative_mode=True, ik_method="dls"), + scale=0.5, + body_offset=DifferentialInverseKinematicsActionCfg.OffsetCfg(pos=[0.0, 0.0, 0.107]), + ) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_instance_randomize_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_instance_randomize_env_cfg.py new file mode 100644 index 0000000000..91d1d02730 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_instance_randomize_env_cfg.py @@ -0,0 +1,38 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from omni.isaac.lab.controllers.differential_ik_cfg import DifferentialIKControllerCfg +from omni.isaac.lab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg +from omni.isaac.lab.utils import configclass + +from . import stack_joint_pos_instance_randomize_env_cfg + +## +# Pre-defined configs +## +from omni.isaac.lab_assets.franka import FRANKA_PANDA_HIGH_PD_CFG # isort: skip + + +@configclass +class FrankaCubeStackInstanceRandomizeEnvCfg( + stack_joint_pos_instance_randomize_env_cfg.FrankaCubeStackInstanceRandomizeEnvCfg +): + def __post_init__(self): + # post init of parent + super().__post_init__() + + # Set Franka as robot + # We switch here to a stiffer PD controller for IK tracking to be better. + self.scene.robot = FRANKA_PANDA_HIGH_PD_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # Set actions for the specific robot type (franka) + self.actions.arm_action = DifferentialInverseKinematicsActionCfg( + asset_name="robot", + joint_names=["panda_joint.*"], + body_name="panda_hand", + controller=DifferentialIKControllerCfg(command_type="pose", use_relative_mode=True, ik_method="dls"), + scale=0.5, + body_offset=DifferentialInverseKinematicsActionCfg.OffsetCfg(pos=[0.0, 0.0, 0.107]), + ) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_env_cfg.py new file mode 100644 index 0000000000..3b7dc1dc2f --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_env_cfg.py @@ -0,0 +1,180 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import RigidObjectCfg +from omni.isaac.lab.managers import EventTermCfg as EventTerm +from omni.isaac.lab.managers import SceneEntityCfg +from omni.isaac.lab.sensors import CameraCfg, FrameTransformerCfg +from omni.isaac.lab.sensors.frame_transformer.frame_transformer_cfg import OffsetCfg +from omni.isaac.lab.sim.schemas.schemas_cfg import RigidBodyPropertiesCfg +from omni.isaac.lab.sim.spawners.from_files.from_files_cfg import UsdFileCfg +from omni.isaac.lab.utils import configclass +from omni.isaac.lab.utils.assets import ISAAC_NUCLEUS_DIR + +from omni.isaac.lab_tasks.manager_based.manipulation.stack import mdp +from omni.isaac.lab_tasks.manager_based.manipulation.stack.mdp import franka_stack_events +from omni.isaac.lab_tasks.manager_based.manipulation.stack.stack_env_cfg import StackEnvCfg + +## +# Pre-defined configs +## +from omni.isaac.lab.markers.config import FRAME_MARKER_CFG # isort: skip +from omni.isaac.lab_assets.franka import FRANKA_PANDA_CFG # isort: skip + + +@configclass +class EventCfg: + """Configuration for events.""" + + init_franka_arm_pose = EventTerm( + func=franka_stack_events.set_default_joint_pose, + mode="startup", + params={ + "default_pose": [0.0444, -0.1894, -0.1107, -2.5148, 0.0044, 2.3775, 0.6952, 0.0400, 0.0400], + }, + ) + + randomize_franka_joint_state = EventTerm( + func=franka_stack_events.randomize_joint_by_gaussian_offset, + mode="reset", + params={ + "mean": 0.0, + "std": 0.02, + "asset_cfg": SceneEntityCfg("robot"), + }, + ) + + randomize_cube_positions = EventTerm( + func=franka_stack_events.randomize_object_pose, + mode="reset", + params={ + "pose_range": {"x": (0.4, 0.6), "y": (-0.10, 0.10), "z": (0.0203, 0.0203), "yaw": (-1.0, 1, 0)}, + "min_separation": 0.1, + "asset_cfgs": [SceneEntityCfg("cube_1"), SceneEntityCfg("cube_2"), SceneEntityCfg("cube_3")], + }, + ) + + +@configclass +class FrankaCubeStackEnvCfg(StackEnvCfg): + def __post_init__(self): + # post init of parent + super().__post_init__() + + # Set events + self.events = EventCfg() + + # Set Franka as robot + self.scene.robot = FRANKA_PANDA_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # Set actions for the specific robot type (franka) + self.actions.arm_action = mdp.JointPositionActionCfg( + asset_name="robot", joint_names=["panda_joint.*"], scale=0.5, use_default_offset=True + ) + self.actions.gripper_action = mdp.BinaryJointPositionActionCfg( + asset_name="robot", + joint_names=["panda_finger.*"], + open_command_expr={"panda_finger_.*": 0.04}, + close_command_expr={"panda_finger_.*": 0.0}, + ) + + # Rigid body properties of each cube + cube_properties = RigidBodyPropertiesCfg( + solver_position_iteration_count=16, + solver_velocity_iteration_count=1, + max_angular_velocity=1000.0, + max_linear_velocity=1000.0, + max_depenetration_velocity=5.0, + disable_gravity=False, + ) + + # Set each stacking cube deterministically + self.scene.cube_1 = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube_1", + init_state=RigidObjectCfg.InitialStateCfg(pos=[0.4, 0.0, 0.0203], rot=[1, 0, 0, 0]), + spawn=UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/blue_block.usd", + scale=(1.0, 1.0, 1.0), + rigid_props=cube_properties, + ), + ) + self.scene.cube_2 = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube_2", + init_state=RigidObjectCfg.InitialStateCfg(pos=[0.55, 0.05, 0.0203], rot=[1, 0, 0, 0]), + spawn=UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/red_block.usd", + scale=(1.0, 1.0, 1.0), + rigid_props=cube_properties, + ), + ) + self.scene.cube_3 = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube_3", + init_state=RigidObjectCfg.InitialStateCfg(pos=[0.60, -0.1, 0.0203], rot=[1, 0, 0, 0]), + spawn=UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/green_block.usd", + scale=(1.0, 1.0, 1.0), + rigid_props=cube_properties, + ), + ) + + # Set wrist camera + self.scene.wrist_cam = CameraCfg( + prim_path="{ENV_REGEX_NS}/Robot/panda_hand/wrist_cam", + update_period=0.0333, + height=84, + width=84, + data_types=["rgb", "distance_to_image_plane"], + spawn=sim_utils.PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 1.0e5) + ), + offset=CameraCfg.OffsetCfg(pos=(0.025, 0.0, 0.0), rot=(0.707, 0.0, 0.0, 0.707), convention="ros"), + ) + + # Set table view camera + self.scene.table_cam = CameraCfg( + prim_path="{ENV_REGEX_NS}/table_cam", + update_period=0.0333, + height=84, + width=84, + data_types=["rgb", "distance_to_image_plane"], + spawn=sim_utils.PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 1.0e5) + ), + offset=CameraCfg.OffsetCfg(pos=(1.0, 0.0, 0.33), rot=(-0.3799, 0.5963, 0.5963, -0.3799), convention="ros"), + ) + + # Listens to the required transforms + marker_cfg = FRAME_MARKER_CFG.copy() + marker_cfg.markers["frame"].scale = (0.1, 0.1, 0.1) + marker_cfg.prim_path = "/Visuals/FrameTransformer" + self.scene.ee_frame = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/panda_link0", + debug_vis=False, + visualizer_cfg=marker_cfg, + target_frames=[ + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/panda_hand", + name="end_effector", + offset=OffsetCfg( + pos=[0.0, 0.0, 0.1034], + ), + ), + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/panda_rightfinger", + name="tool_rightfinger", + offset=OffsetCfg( + pos=(0.0, 0.0, 0.046), + ), + ), + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/panda_leftfinger", + name="tool_leftfinger", + offset=OffsetCfg( + pos=(0.0, 0.0, 0.046), + ), + ), + ], + ) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_instance_randomize_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_instance_randomize_env_cfg.py new file mode 100644 index 0000000000..f9038c8ae9 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_instance_randomize_env_cfg.py @@ -0,0 +1,224 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import torch + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import RigidObjectCfg, RigidObjectCollectionCfg +from omni.isaac.lab.managers import EventTermCfg as EventTerm +from omni.isaac.lab.managers import SceneEntityCfg +from omni.isaac.lab.sensors import CameraCfg, FrameTransformerCfg +from omni.isaac.lab.sensors.frame_transformer.frame_transformer_cfg import OffsetCfg +from omni.isaac.lab.sim.schemas.schemas_cfg import RigidBodyPropertiesCfg +from omni.isaac.lab.sim.spawners.from_files.from_files_cfg import UsdFileCfg +from omni.isaac.lab.utils import configclass +from omni.isaac.lab.utils.assets import ISAAC_NUCLEUS_DIR + +from omni.isaac.lab_tasks.manager_based.manipulation.stack import mdp +from omni.isaac.lab_tasks.manager_based.manipulation.stack.mdp import franka_stack_events +from omni.isaac.lab_tasks.manager_based.manipulation.stack.stack_instance_randomize_env_cfg import ( + StackInstanceRandomizeEnvCfg, +) + +## +# Pre-defined configs +## +from omni.isaac.lab.markers.config import FRAME_MARKER_CFG # isort: skip +from omni.isaac.lab_assets.franka import FRANKA_PANDA_CFG # isort: skip + + +@configclass +class EventCfg: + """Configuration for events.""" + + init_franka_arm_pose = EventTerm( + func=franka_stack_events.set_default_joint_pose, + mode="startup", + params={ + "default_pose": [0.0444, -0.1894, -0.1107, -2.5148, 0.0044, 2.3775, 0.6952, 0.0400, 0.0400], + }, + ) + + randomize_franka_joint_state = EventTerm( + func=franka_stack_events.randomize_joint_by_gaussian_offset, + mode="reset", + params={ + "mean": 0.0, + "std": 0.02, + "asset_cfg": SceneEntityCfg("robot"), + }, + ) + + randomize_cubes_in_focus = EventTerm( + func=franka_stack_events.randomize_rigid_objects_in_focus, + mode="reset", + params={ + "asset_cfgs": [SceneEntityCfg("cube_1"), SceneEntityCfg("cube_2"), SceneEntityCfg("cube_3")], + "out_focus_state": torch.tensor([10.0, 10.0, 10.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + "pose_range": {"x": (0.4, 0.6), "y": (-0.10, 0.10), "z": (0.0203, 0.0203), "yaw": (-1.0, 1, 0)}, + "min_separation": 0.1, + }, + ) + + +@configclass +class FrankaCubeStackInstanceRandomizeEnvCfg(StackInstanceRandomizeEnvCfg): + def __post_init__(self): + # post init of parent + super().__post_init__() + + # Set events + self.events = EventCfg() + + # Set Franka as robot + self.scene.robot = FRANKA_PANDA_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # Set actions for the specific robot type (franka) + self.actions.arm_action = mdp.JointPositionActionCfg( + asset_name="robot", joint_names=["panda_joint.*"], scale=0.5, use_default_offset=True + ) + self.actions.gripper_action = mdp.BinaryJointPositionActionCfg( + asset_name="robot", + joint_names=["panda_finger.*"], + open_command_expr={"panda_finger_.*": 0.04}, + close_command_expr={"panda_finger_.*": 0.0}, + ) + + # Rigid body properties of each cube + cube_properties = RigidBodyPropertiesCfg( + solver_position_iteration_count=16, + solver_velocity_iteration_count=1, + max_angular_velocity=1000.0, + max_linear_velocity=1000.0, + max_depenetration_velocity=5.0, + disable_gravity=False, + ) + + # Set each stacking cube to be a collection of rigid objects + cube_1_config_dict = { + "blue_cube": RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube_1_Blue", + init_state=RigidObjectCfg.InitialStateCfg(pos=[0.4, 0.0, 0.0203], rot=[1, 0, 0, 0]), + spawn=UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/blue_block.usd", + scale=(1.0, 1.0, 1.0), + rigid_props=cube_properties, + ), + ), + "red_cube": RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube_1_Red", + init_state=RigidObjectCfg.InitialStateCfg(pos=[0.4, 0.0, 0.0403], rot=[1, 0, 0, 0]), + spawn=UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/red_block.usd", + scale=(1.0, 1.0, 1.0), + rigid_props=cube_properties, + ), + ), + } + + cube_2_config_dict = { + "red_cube": RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube_2_Red", + init_state=RigidObjectCfg.InitialStateCfg(pos=[0.55, 0.05, 0.0203], rot=[1, 0, 0, 0]), + spawn=UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/red_block.usd", + scale=(1.0, 1.0, 1.0), + rigid_props=cube_properties, + ), + ), + "yellow_cube": RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube_2_Yellow", + init_state=RigidObjectCfg.InitialStateCfg(pos=[0.55, 0.05, 0.0403], rot=[1, 0, 0, 0]), + spawn=UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/yellow_block.usd", + scale=(1.0, 1.0, 1.0), + rigid_props=cube_properties, + ), + ), + } + + cube_3_config_dict = { + "yellow_cube": RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube_3_Yellow", + init_state=RigidObjectCfg.InitialStateCfg(pos=[0.60, -0.1, 0.0203], rot=[1, 0, 0, 0]), + spawn=UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/yellow_block.usd", + scale=(1.0, 1.0, 1.0), + rigid_props=cube_properties, + ), + ), + "green_cube": RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube_2_Green", + init_state=RigidObjectCfg.InitialStateCfg(pos=[0.60, -0.1, 0.0403], rot=[1, 0, 0, 0]), + spawn=UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/green_block.usd", + scale=(1.0, 1.0, 1.0), + rigid_props=cube_properties, + ), + ), + } + + self.scene.cube_1 = RigidObjectCollectionCfg(rigid_objects=cube_1_config_dict) + self.scene.cube_2 = RigidObjectCollectionCfg(rigid_objects=cube_2_config_dict) + self.scene.cube_3 = RigidObjectCollectionCfg(rigid_objects=cube_3_config_dict) + + # Set wrist camera + self.scene.wrist_cam = CameraCfg( + prim_path="{ENV_REGEX_NS}/Robot/panda_hand/wrist_cam", + update_period=0.0333, + height=84, + width=84, + data_types=["rgb", "distance_to_image_plane"], + spawn=sim_utils.PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 1.0e5) + ), + offset=CameraCfg.OffsetCfg(pos=(0.025, 0.0, 0.0), rot=(0.707, 0.0, 0.0, 0.707), convention="ros"), + ) + + # Set table view camera + self.scene.table_cam = CameraCfg( + prim_path="{ENV_REGEX_NS}/table_cam", + update_period=0.0333, + height=84, + width=84, + data_types=["rgb", "distance_to_image_plane"], + spawn=sim_utils.PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 1.0e5) + ), + offset=CameraCfg.OffsetCfg(pos=(1.0, 0.0, 0.33), rot=(-0.3799, 0.5963, 0.5963, -0.3799), convention="ros"), + ) + + # Listens to the required transforms + marker_cfg = FRAME_MARKER_CFG.copy() + marker_cfg.markers["frame"].scale = (0.1, 0.1, 0.1) + marker_cfg.prim_path = "/Visuals/FrameTransformer" + self.scene.ee_frame = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/panda_link0", + debug_vis=False, + visualizer_cfg=marker_cfg, + target_frames=[ + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/panda_hand", + name="end_effector", + offset=OffsetCfg( + pos=[0.0, 0.0, 0.1034], + ), + ), + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/panda_rightfinger", + name="tool_rightfinger", + offset=OffsetCfg( + pos=(0.0, 0.0, 0.046), + ), + ), + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/panda_leftfinger", + name="tool_leftfinger", + offset=OffsetCfg( + pos=(0.0, 0.0, 0.046), + ), + ), + ], + ) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/mdp/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/mdp/__init__.py new file mode 100644 index 0000000000..8a6d3a1a71 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/mdp/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""This sub-module contains the functions that are specific to the lift environments.""" + +from omni.isaac.lab.envs.mdp import * # noqa: F401, F403 + +from .observations import * # noqa: F401, F403 +from .terminations import * # noqa: F401, F403 diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/mdp/franka_stack_events.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/mdp/franka_stack_events.py new file mode 100644 index 0000000000..f741470a34 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/mdp/franka_stack_events.py @@ -0,0 +1,186 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + + +from __future__ import annotations + +import math +import random +import torch +from typing import TYPE_CHECKING + +import omni.isaac.lab.utils.math as math_utils +from omni.isaac.lab.assets import Articulation, AssetBase +from omni.isaac.lab.managers import SceneEntityCfg + +if TYPE_CHECKING: + from omni.isaac.lab.envs import ManagerBasedEnv + + +def set_default_joint_pose( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + default_pose: torch.Tensor, + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + # Set the default pose for robots in all envs + asset = env.scene[asset_cfg.name] + asset.data.default_joint_pos = torch.tensor(default_pose, device=env.device).repeat(env.num_envs, 1) + + +def randomize_joint_by_gaussian_offset( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + mean: float, + std: float, + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + asset: Articulation = env.scene[asset_cfg.name] + + # Add gaussian noise to joint states + joint_pos = asset.data.default_joint_pos[env_ids].clone() + joint_vel = asset.data.default_joint_vel[env_ids].clone() + joint_pos += math_utils.sample_gaussian(mean, std, joint_pos.shape, joint_pos.device) + + # Clamp joint pos to limits + joint_pos_limits = asset.data.soft_joint_pos_limits[env_ids] + joint_pos = joint_pos.clamp_(joint_pos_limits[..., 0], joint_pos_limits[..., 1]) + + # Don't noise the gripper poses + joint_pos[:, -2:] = asset.data.default_joint_pos[env_ids, -2:] + + # Set into the physics simulation + asset.set_joint_position_target(joint_pos, env_ids=env_ids) + asset.set_joint_velocity_target(joint_vel, env_ids=env_ids) + asset.write_joint_state_to_sim(joint_pos, joint_vel, env_ids=env_ids) + + +def randomize_scene_lighting_domelight( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + intensity_range: tuple[float, float], + asset_cfg: SceneEntityCfg = SceneEntityCfg("light"), +): + asset: AssetBase = env.scene[asset_cfg.name] + light_prim = asset.prims[0] + + # Sample new light intensity + new_intensity = random.uniform(intensity_range[0], intensity_range[1]) + + # Set light intensity to light prim + intensity_attr = light_prim.GetAttribute("inputs:intensity") + intensity_attr.Set(new_intensity) + + +def sample_object_poses( + num_objects: int, + min_separation: float = 0.0, + pose_range: dict[str, tuple[float, float]] = {}, + max_sample_tries: int = 5000, +): + range_list = [pose_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + pose_list = [] + + for i in range(num_objects): + for j in range(max_sample_tries): + sample = [random.uniform(range[0], range[1]) for range in range_list] + + # Accept pose if it is the first one, or if reached max num tries + if len(pose_list) == 0 or j == max_sample_tries - 1: + pose_list.append(sample) + break + + # Check if pose of object is sufficiently far away from all other objects + separation_check = [math.dist(sample[:3], pose[:3]) > min_separation for pose in pose_list] + if False not in separation_check: + pose_list.append(sample) + break + + return pose_list + + +def randomize_object_pose( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + asset_cfgs: list[SceneEntityCfg], + min_separation: float = 0.0, + pose_range: dict[str, tuple[float, float]] = {}, + max_sample_tries: int = 5000, +): + if env_ids is None: + return + + # Randomize poses in each environment independently + for cur_env in env_ids.tolist(): + pose_list = sample_object_poses( + num_objects=len(asset_cfgs), + min_separation=min_separation, + pose_range=pose_range, + max_sample_tries=max_sample_tries, + ) + + # Randomize pose for each object + for i in range(len(asset_cfgs)): + asset_cfg = asset_cfgs[i] + asset = env.scene[asset_cfg.name] + + # Write pose to simulation + pose_tensor = torch.tensor([pose_list[i]], device=env.device) + positions = pose_tensor[:, 0:3] + env.scene.env_origins[cur_env, 0:3] + orientations = math_utils.quat_from_euler_xyz(pose_tensor[:, 3], pose_tensor[:, 4], pose_tensor[:, 5]) + asset.write_root_pose_to_sim( + torch.cat([positions, orientations], dim=-1), env_ids=torch.tensor([cur_env], device=env.device) + ) + asset.write_root_velocity_to_sim( + torch.zeros(1, 6, device=env.device), env_ids=torch.tensor([cur_env], device=env.device) + ) + + +def randomize_rigid_objects_in_focus( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + asset_cfgs: list[SceneEntityCfg], + out_focus_state: torch.Tensor, + min_separation: float = 0.0, + pose_range: dict[str, tuple[float, float]] = {}, + max_sample_tries: int = 5000, +): + if env_ids is None: + return + + # List of rigid objects in focus for each env (dim = [num_envs, num_rigid_objects]) + env.rigid_objects_in_focus = [] + + for cur_env in env_ids.tolist(): + # Sample in focus object poses + pose_list = sample_object_poses( + num_objects=len(asset_cfgs), + min_separation=min_separation, + pose_range=pose_range, + max_sample_tries=max_sample_tries, + ) + + selected_ids = [] + for asset_idx in range(len(asset_cfgs)): + asset_cfg = asset_cfgs[asset_idx] + asset = env.scene[asset_cfg.name] + + # Randomly select an object to bring into focus + object_id = random.randint(0, asset.num_objects - 1) + selected_ids.append(object_id) + + # Create object state tensor + object_states = torch.stack([out_focus_state] * asset.num_objects).to(device=env.device) + pose_tensor = torch.tensor([pose_list[asset_idx]], device=env.device) + positions = pose_tensor[:, 0:3] + env.scene.env_origins[cur_env, 0:3] + orientations = math_utils.quat_from_euler_xyz(pose_tensor[:, 3], pose_tensor[:, 4], pose_tensor[:, 5]) + object_states[object_id, 0:3] = positions + object_states[object_id, 3:7] = orientations + + asset.write_object_state_to_sim( + object_state=object_states, env_ids=torch.tensor([cur_env], device=env.device) + ) + + env.rigid_objects_in_focus.append(selected_ids) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/mdp/observations.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/mdp/observations.py new file mode 100644 index 0000000000..54831e9d75 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/mdp/observations.py @@ -0,0 +1,326 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +from omni.isaac.lab.assets import Articulation, RigidObject, RigidObjectCollection +from omni.isaac.lab.managers import SceneEntityCfg +from omni.isaac.lab.sensors import FrameTransformer + +if TYPE_CHECKING: + from omni.isaac.lab.envs import ManagerBasedRLEnv + + +def cube_positions_in_world_frame( + env: ManagerBasedRLEnv, + cube_1_cfg: SceneEntityCfg = SceneEntityCfg("cube_1"), + cube_2_cfg: SceneEntityCfg = SceneEntityCfg("cube_2"), + cube_3_cfg: SceneEntityCfg = SceneEntityCfg("cube_3"), +) -> torch.Tensor: + """The position of the cubes in the world frame.""" + cube_1: RigidObject = env.scene[cube_1_cfg.name] + cube_2: RigidObject = env.scene[cube_2_cfg.name] + cube_3: RigidObject = env.scene[cube_3_cfg.name] + + return torch.cat((cube_1.data.root_pos_w, cube_2.data.root_pos_w, cube_3.data.root_pos_w), dim=1) + + +def instance_randomize_cube_positions_in_world_frame( + env: ManagerBasedRLEnv, + cube_1_cfg: SceneEntityCfg = SceneEntityCfg("cube_1"), + cube_2_cfg: SceneEntityCfg = SceneEntityCfg("cube_2"), + cube_3_cfg: SceneEntityCfg = SceneEntityCfg("cube_3"), +) -> torch.Tensor: + """The position of the cubes in the world frame.""" + if not hasattr(env, "rigid_objects_in_focus"): + return torch.full((env.num_envs, 9), fill_value=-1) + + cube_1: RigidObjectCollection = env.scene[cube_1_cfg.name] + cube_2: RigidObjectCollection = env.scene[cube_2_cfg.name] + cube_3: RigidObjectCollection = env.scene[cube_3_cfg.name] + + cube_1_pos_w = [] + cube_2_pos_w = [] + cube_3_pos_w = [] + for env_id in range(env.num_envs): + cube_1_pos_w.append(cube_1.data.object_pos_w[env_id, env.rigid_objects_in_focus[env_id][0], :3]) + cube_2_pos_w.append(cube_2.data.object_pos_w[env_id, env.rigid_objects_in_focus[env_id][1], :3]) + cube_3_pos_w.append(cube_3.data.object_pos_w[env_id, env.rigid_objects_in_focus[env_id][2], :3]) + cube_1_pos_w = torch.stack(cube_1_pos_w) + cube_2_pos_w = torch.stack(cube_2_pos_w) + cube_3_pos_w = torch.stack(cube_3_pos_w) + + return torch.cat((cube_1_pos_w, cube_2_pos_w, cube_3_pos_w), dim=1) + + +def cube_orientations_in_world_frame( + env: ManagerBasedRLEnv, + cube_1_cfg: SceneEntityCfg = SceneEntityCfg("cube_1"), + cube_2_cfg: SceneEntityCfg = SceneEntityCfg("cube_2"), + cube_3_cfg: SceneEntityCfg = SceneEntityCfg("cube_3"), +): + """The orientation of the cubes in the world frame.""" + cube_1: RigidObject = env.scene[cube_1_cfg.name] + cube_2: RigidObject = env.scene[cube_2_cfg.name] + cube_3: RigidObject = env.scene[cube_3_cfg.name] + + return torch.cat((cube_1.data.root_quat_w, cube_2.data.root_quat_w, cube_3.data.root_quat_w), dim=1) + + +def instance_randomize_cube_orientations_in_world_frame( + env: ManagerBasedRLEnv, + cube_1_cfg: SceneEntityCfg = SceneEntityCfg("cube_1"), + cube_2_cfg: SceneEntityCfg = SceneEntityCfg("cube_2"), + cube_3_cfg: SceneEntityCfg = SceneEntityCfg("cube_3"), +) -> torch.Tensor: + """The orientation of the cubes in the world frame.""" + if not hasattr(env, "rigid_objects_in_focus"): + return torch.full((env.num_envs, 9), fill_value=-1) + + cube_1: RigidObjectCollection = env.scene[cube_1_cfg.name] + cube_2: RigidObjectCollection = env.scene[cube_2_cfg.name] + cube_3: RigidObjectCollection = env.scene[cube_3_cfg.name] + + cube_1_quat_w = [] + cube_2_quat_w = [] + cube_3_quat_w = [] + for env_id in range(env.num_envs): + cube_1_quat_w.append(cube_1.data.object_quat_w[env_id, env.rigid_objects_in_focus[env_id][0], :4]) + cube_2_quat_w.append(cube_2.data.object_quat_w[env_id, env.rigid_objects_in_focus[env_id][1], :4]) + cube_3_quat_w.append(cube_3.data.object_quat_w[env_id, env.rigid_objects_in_focus[env_id][2], :4]) + cube_1_quat_w = torch.stack(cube_1_quat_w) + cube_2_quat_w = torch.stack(cube_2_quat_w) + cube_3_quat_w = torch.stack(cube_3_quat_w) + + return torch.cat((cube_1_quat_w, cube_2_quat_w, cube_3_quat_w), dim=1) + + +def object_obs( + env: ManagerBasedRLEnv, + cube_1_cfg: SceneEntityCfg = SceneEntityCfg("cube_1"), + cube_2_cfg: SceneEntityCfg = SceneEntityCfg("cube_2"), + cube_3_cfg: SceneEntityCfg = SceneEntityCfg("cube_3"), + ee_frame_cfg: SceneEntityCfg = SceneEntityCfg("ee_frame"), +): + """ + Object observations (in world frame): + cube_1 pos, + cube_1 quat, + cube_2 pos, + cube_2 quat, + cube_3 pos, + cube_3 quat, + gripper to cube_1, + gripper to cube_2, + gripper to cube_3, + cube_1 to cube_2, + cube_2 to cube_3, + cube_1 to cube_3, + """ + cube_1: RigidObject = env.scene[cube_1_cfg.name] + cube_2: RigidObject = env.scene[cube_2_cfg.name] + cube_3: RigidObject = env.scene[cube_3_cfg.name] + ee_frame: FrameTransformer = env.scene[ee_frame_cfg.name] + + cube_1_pos_w = cube_1.data.root_pos_w + cube_1_quat_w = cube_1.data.root_quat_w + + cube_2_pos_w = cube_2.data.root_pos_w + cube_2_quat_w = cube_2.data.root_quat_w + + cube_3_pos_w = cube_3.data.root_pos_w + cube_3_quat_w = cube_3.data.root_quat_w + + ee_pos_w = ee_frame.data.target_pos_w[:, 0, :] + gripper_to_cube_1 = cube_1_pos_w - ee_pos_w + gripper_to_cube_2 = cube_2_pos_w - ee_pos_w + gripper_to_cube_3 = cube_3_pos_w - ee_pos_w + + cube_1_to_2 = cube_1_pos_w - cube_2_pos_w + cube_2_to_3 = cube_2_pos_w - cube_3_pos_w + cube_1_to_3 = cube_1_pos_w - cube_3_pos_w + + return torch.cat( + ( + cube_1_pos_w, + cube_1_quat_w, + cube_2_pos_w, + cube_2_quat_w, + cube_3_pos_w, + cube_3_quat_w, + gripper_to_cube_1, + gripper_to_cube_2, + gripper_to_cube_3, + cube_1_to_2, + cube_2_to_3, + cube_1_to_3, + ), + dim=1, + ) + + +def instance_randomize_object_obs( + env: ManagerBasedRLEnv, + cube_1_cfg: SceneEntityCfg = SceneEntityCfg("cube_1"), + cube_2_cfg: SceneEntityCfg = SceneEntityCfg("cube_2"), + cube_3_cfg: SceneEntityCfg = SceneEntityCfg("cube_3"), + ee_frame_cfg: SceneEntityCfg = SceneEntityCfg("ee_frame"), +): + """ + Object observations (in world frame): + cube_1 pos, + cube_1 quat, + cube_2 pos, + cube_2 quat, + cube_3 pos, + cube_3 quat, + gripper to cube_1, + gripper to cube_2, + gripper to cube_3, + cube_1 to cube_2, + cube_2 to cube_3, + cube_1 to cube_3, + """ + if not hasattr(env, "rigid_objects_in_focus"): + return torch.full((env.num_envs, 9), fill_value=-1) + + cube_1: RigidObjectCollection = env.scene[cube_1_cfg.name] + cube_2: RigidObjectCollection = env.scene[cube_2_cfg.name] + cube_3: RigidObjectCollection = env.scene[cube_3_cfg.name] + ee_frame: FrameTransformer = env.scene[ee_frame_cfg.name] + + cube_1_pos_w = [] + cube_2_pos_w = [] + cube_3_pos_w = [] + cube_1_quat_w = [] + cube_2_quat_w = [] + cube_3_quat_w = [] + for env_id in range(env.num_envs): + cube_1_pos_w.append(cube_1.data.object_pos_w[env_id, env.rigid_objects_in_focus[env_id][0], :3]) + cube_2_pos_w.append(cube_2.data.object_pos_w[env_id, env.rigid_objects_in_focus[env_id][1], :3]) + cube_3_pos_w.append(cube_3.data.object_pos_w[env_id, env.rigid_objects_in_focus[env_id][2], :3]) + cube_1_quat_w.append(cube_1.data.object_quat_w[env_id, env.rigid_objects_in_focus[env_id][0], :4]) + cube_2_quat_w.append(cube_2.data.object_quat_w[env_id, env.rigid_objects_in_focus[env_id][1], :4]) + cube_3_quat_w.append(cube_3.data.object_quat_w[env_id, env.rigid_objects_in_focus[env_id][2], :4]) + cube_1_pos_w = torch.stack(cube_1_pos_w) + cube_2_pos_w = torch.stack(cube_2_pos_w) + cube_3_pos_w = torch.stack(cube_3_pos_w) + cube_1_quat_w = torch.stack(cube_1_quat_w) + cube_2_quat_w = torch.stack(cube_2_quat_w) + cube_3_quat_w = torch.stack(cube_3_quat_w) + + ee_pos_w = ee_frame.data.target_pos_w[:, 0, :] + gripper_to_cube_1 = cube_1_pos_w - ee_pos_w + gripper_to_cube_2 = cube_2_pos_w - ee_pos_w + gripper_to_cube_3 = cube_3_pos_w - ee_pos_w + + cube_1_to_2 = cube_1_pos_w - cube_2_pos_w + cube_2_to_3 = cube_2_pos_w - cube_3_pos_w + cube_1_to_3 = cube_1_pos_w - cube_3_pos_w + + return torch.cat( + ( + cube_1_pos_w, + cube_1_quat_w, + cube_2_pos_w, + cube_2_quat_w, + cube_3_pos_w, + cube_3_quat_w, + gripper_to_cube_1, + gripper_to_cube_2, + gripper_to_cube_3, + cube_1_to_2, + cube_2_to_3, + cube_1_to_3, + ), + dim=1, + ) + + +def ee_frame_pos(env: ManagerBasedRLEnv, ee_frame_cfg: SceneEntityCfg = SceneEntityCfg("ee_frame")) -> torch.Tensor: + ee_frame: FrameTransformer = env.scene[ee_frame_cfg.name] + ee_frame_pos = ee_frame.data.target_pos_w[:, 0, :] + + return ee_frame_pos + + +def ee_frame_quat(env: ManagerBasedRLEnv, ee_frame_cfg: SceneEntityCfg = SceneEntityCfg("ee_frame")) -> torch.Tensor: + ee_frame: FrameTransformer = env.scene[ee_frame_cfg.name] + ee_frame_quat = ee_frame.data.target_quat_w[:, 0, :] + + return ee_frame_quat + + +def gripper_pos(env: ManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + robot: Articulation = env.scene[robot_cfg.name] + finger_joint_1 = robot.data.joint_pos[:, -1].clone().unsqueeze(1) + finger_joint_2 = -1 * robot.data.joint_pos[:, -2].clone().unsqueeze(1) + + return torch.cat((finger_joint_1, finger_joint_2), dim=1) + + +def object_grasped( + env: ManagerBasedRLEnv, + robot_cfg: SceneEntityCfg, + ee_frame_cfg: SceneEntityCfg, + object_cfg: SceneEntityCfg, + diff_threshold: float = 0.06, + gripper_open_val: torch.tensor = torch.tensor([0.04]), + gripper_threshold: float = 0.005, +) -> torch.Tensor: + """Check if an object is grasped by the specified robot.""" + + robot: Articulation = env.scene[robot_cfg.name] + ee_frame: FrameTransformer = env.scene[ee_frame_cfg.name] + object: RigidObject = env.scene[object_cfg.name] + + object_pos = object.data.root_pos_w + end_effector_pos = ee_frame.data.target_pos_w[:, 0, :] + pose_diff = torch.linalg.vector_norm(object_pos - end_effector_pos, dim=1) + + grasped = torch.logical_and( + pose_diff < diff_threshold, + torch.abs(robot.data.joint_pos[:, -1] - gripper_open_val.to(env.device)) > gripper_threshold, + ) + grasped = torch.logical_and( + grasped, torch.abs(robot.data.joint_pos[:, -2] - gripper_open_val.to(env.device)) > gripper_threshold + ) + + return grasped + + +def object_stacked( + env: ManagerBasedRLEnv, + robot_cfg: SceneEntityCfg, + upper_object_cfg: SceneEntityCfg, + lower_object_cfg: SceneEntityCfg, + xy_threshold: float = 0.05, + height_threshold: float = 0.005, + height_diff: float = 0.0468, + gripper_open_val: torch.tensor = torch.tensor([0.04]), +) -> torch.Tensor: + """Check if an object is stacked by the specified robot.""" + + robot: Articulation = env.scene[robot_cfg.name] + upper_object: RigidObject = env.scene[upper_object_cfg.name] + lower_object: RigidObject = env.scene[lower_object_cfg.name] + + pos_diff = upper_object.data.root_pos_w - lower_object.data.root_pos_w + height_dist = torch.linalg.vector_norm(pos_diff[:, 2:], dim=1) + xy_dist = torch.linalg.vector_norm(pos_diff[:, :2], dim=1) + + stacked = torch.logical_and(xy_dist < xy_threshold, (height_dist - height_diff) < height_threshold) + + stacked = torch.logical_and( + torch.isclose(robot.data.joint_pos[:, -1], gripper_open_val.to(env.device), atol=1e-4, rtol=1e-4), stacked + ) + stacked = torch.logical_and( + torch.isclose(robot.data.joint_pos[:, -2], gripper_open_val.to(env.device), atol=1e-4, rtol=1e-4), stacked + ) + + return stacked diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/mdp/terminations.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/mdp/terminations.py new file mode 100644 index 0000000000..9e5e8e161a --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/mdp/terminations.py @@ -0,0 +1,66 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Common functions that can be used to activate certain terminations for the lift task. + +The functions can be passed to the :class:`omni.isaac.lab.managers.TerminationTermCfg` object to enable +the termination introduced by the function. +""" + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +from omni.isaac.lab.assets import Articulation, RigidObject +from omni.isaac.lab.managers import SceneEntityCfg + +if TYPE_CHECKING: + from omni.isaac.lab.envs import ManagerBasedRLEnv + + +def cubes_stacked( + env: ManagerBasedRLEnv, + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + cube_1_cfg: SceneEntityCfg = SceneEntityCfg("cube_1"), + cube_2_cfg: SceneEntityCfg = SceneEntityCfg("cube_2"), + cube_3_cfg: SceneEntityCfg = SceneEntityCfg("cube_3"), + xy_threshold: float = 0.05, + height_threshold: float = 0.005, + height_diff: float = 0.0468, + gripper_open_val: torch.tensor = torch.tensor([0.04]), + atol=0.0001, + rtol=0.0001, +): + robot: Articulation = env.scene[robot_cfg.name] + cube_1: RigidObject = env.scene[cube_1_cfg.name] + cube_2: RigidObject = env.scene[cube_2_cfg.name] + cube_3: RigidObject = env.scene[cube_3_cfg.name] + + pos_diff_c12 = cube_1.data.root_pos_w - cube_2.data.root_pos_w + pos_diff_c23 = cube_2.data.root_pos_w - cube_3.data.root_pos_w + + # Compute cube position difference in x-y plane + xy_dist_c12 = torch.norm(pos_diff_c12[:, :2], dim=1) + xy_dist_c23 = torch.norm(pos_diff_c23[:, :2], dim=1) + + # Compute cube height difference + h_dist_c12 = torch.norm(pos_diff_c12[:, 2:], dim=1) + h_dist_c23 = torch.norm(pos_diff_c23[:, 2:], dim=1) + + # Check cube positions + stacked = torch.logical_and(xy_dist_c12 < xy_threshold, xy_dist_c23 < xy_threshold) + stacked = torch.logical_and(h_dist_c12 - height_diff < height_threshold, stacked) + stacked = torch.logical_and(h_dist_c23 - height_diff < height_threshold, stacked) + + # Check gripper positions + stacked = torch.logical_and( + torch.isclose(robot.data.joint_pos[:, -1], gripper_open_val.to(env.device), atol=atol, rtol=rtol), stacked + ) + stacked = torch.logical_and( + torch.isclose(robot.data.joint_pos[:, -2], gripper_open_val.to(env.device), atol=atol, rtol=rtol), stacked + ) + + return stacked diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/stack_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/stack_env_cfg.py new file mode 100644 index 0000000000..6d07861a59 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/stack_env_cfg.py @@ -0,0 +1,205 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import ArticulationCfg, AssetBaseCfg +from omni.isaac.lab.envs import ManagerBasedRLEnvCfg +from omni.isaac.lab.managers import ObservationGroupCfg as ObsGroup +from omni.isaac.lab.managers import ObservationTermCfg as ObsTerm +from omni.isaac.lab.managers import SceneEntityCfg +from omni.isaac.lab.managers import TerminationTermCfg as DoneTerm +from omni.isaac.lab.scene import InteractiveSceneCfg +from omni.isaac.lab.sensors import CameraCfg +from omni.isaac.lab.sensors.frame_transformer.frame_transformer_cfg import FrameTransformerCfg +from omni.isaac.lab.sim.spawners.from_files.from_files_cfg import GroundPlaneCfg, UsdFileCfg +from omni.isaac.lab.utils import configclass +from omni.isaac.lab.utils.assets import ISAAC_NUCLEUS_DIR + +from . import mdp + + +## +# Scene definition +## +@configclass +class ObjectTableSceneCfg(InteractiveSceneCfg): + """Configuration for the lift scene with a robot and a object. + This is the abstract base implementation, the exact scene is defined in the derived classes + which need to set the target object, robot and end-effector frames + """ + + # robots: will be populated by agent env cfg + robot: ArticulationCfg = MISSING + # end-effector sensor: will be populated by agent env cfg + ee_frame: FrameTransformerCfg = MISSING + + # Cameras + wrist_cam: CameraCfg = MISSING + table_cam: CameraCfg = MISSING + + # Table + table = AssetBaseCfg( + prim_path="{ENV_REGEX_NS}/Table", + init_state=AssetBaseCfg.InitialStateCfg(pos=[0.5, 0, 0], rot=[0.707, 0, 0, 0.707]), + spawn=UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd"), + ) + + # plane + plane = AssetBaseCfg( + prim_path="/World/GroundPlane", + init_state=AssetBaseCfg.InitialStateCfg(pos=[0, 0, -1.05]), + spawn=GroundPlaneCfg(), + ) + + # lights + light = AssetBaseCfg( + prim_path="/World/light", + spawn=sim_utils.DomeLightCfg(color=(0.75, 0.75, 0.75), intensity=3000.0), + ) + + +## +# MDP settings +## +@configclass +class ActionsCfg: + """Action specifications for the MDP.""" + + # will be set by agent env cfg + arm_action: mdp.JointPositionActionCfg = MISSING + gripper_action: mdp.BinaryJointPositionActionCfg = MISSING + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group with state values.""" + + actions = ObsTerm(func=mdp.last_action) + joint_pos = ObsTerm(func=mdp.joint_pos_rel) + joint_vel = ObsTerm(func=mdp.joint_vel_rel) + object = ObsTerm(func=mdp.object_obs) + cube_positions = ObsTerm(func=mdp.cube_positions_in_world_frame) + cube_orientations = ObsTerm(func=mdp.cube_orientations_in_world_frame) + eef_pos = ObsTerm(func=mdp.ee_frame_pos) + eef_quat = ObsTerm(func=mdp.ee_frame_quat) + gripper_pos = ObsTerm(func=mdp.gripper_pos) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = False + + @configclass + class RGBCameraPolicyCfg(ObsGroup): + """Observations for policy group with RGB images.""" + + table_cam = ObsTerm( + func=mdp.image, params={"sensor_cfg": SceneEntityCfg("table_cam"), "data_type": "rgb", "normalize": False} + ) + wrist_cam = ObsTerm( + func=mdp.image, params={"sensor_cfg": SceneEntityCfg("wrist_cam"), "data_type": "rgb", "normalize": False} + ) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = False + + @configclass + class SubtaskCfg(ObsGroup): + """Observations for subtask group.""" + + grasp_1 = ObsTerm( + func=mdp.object_grasped, + params={ + "robot_cfg": SceneEntityCfg("robot"), + "ee_frame_cfg": SceneEntityCfg("ee_frame"), + "object_cfg": SceneEntityCfg("cube_2"), + }, + ) + stack_1 = ObsTerm( + func=mdp.object_stacked, + params={ + "robot_cfg": SceneEntityCfg("robot"), + "upper_object_cfg": SceneEntityCfg("cube_2"), + "lower_object_cfg": SceneEntityCfg("cube_1"), + }, + ) + grasp_2 = ObsTerm( + func=mdp.object_grasped, + params={ + "robot_cfg": SceneEntityCfg("robot"), + "ee_frame_cfg": SceneEntityCfg("ee_frame"), + "object_cfg": SceneEntityCfg("cube_3"), + }, + ) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = False + + # observation groups + policy: PolicyCfg = PolicyCfg() + rgb_camera: RGBCameraPolicyCfg = RGBCameraPolicyCfg() + subtask_terms: SubtaskCfg = SubtaskCfg() + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + + cube_1_dropping = DoneTerm( + func=mdp.root_height_below_minimum, params={"minimum_height": -0.05, "asset_cfg": SceneEntityCfg("cube_1")} + ) + + cube_2_dropping = DoneTerm( + func=mdp.root_height_below_minimum, params={"minimum_height": -0.05, "asset_cfg": SceneEntityCfg("cube_2")} + ) + + cube_3_dropping = DoneTerm( + func=mdp.root_height_below_minimum, params={"minimum_height": -0.05, "asset_cfg": SceneEntityCfg("cube_3")} + ) + + success = DoneTerm(func=mdp.cubes_stacked) + + +@configclass +class StackEnvCfg(ManagerBasedRLEnvCfg): + """Configuration for the stacking environment.""" + + # Scene settings + scene: ObjectTableSceneCfg = ObjectTableSceneCfg(num_envs=4096, env_spacing=2.5, replicate_physics=False) + # Basic settings + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + # MDP settings + terminations: TerminationsCfg = TerminationsCfg() + + # Unused managers + commands = None + rewards = None + events = None + curriculum = None + + def __post_init__(self): + """Post initialization.""" + # general settings + self.decimation = 5 + self.episode_length_s = 30.0 + # simulation settings + self.sim.dt = 0.01 # 100Hz + self.sim.render_interval = self.decimation + + self.sim.physx.bounce_threshold_velocity = 0.2 + self.sim.physx.bounce_threshold_velocity = 0.01 + self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 + self.sim.physx.gpu_total_aggregate_pairs_capacity = 16 * 1024 + self.sim.physx.friction_correlation_distance = 0.00625 diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/stack_instance_randomize_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/stack_instance_randomize_env_cfg.py new file mode 100644 index 0000000000..1a5766b918 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/manipulation/stack/stack_instance_randomize_env_cfg.py @@ -0,0 +1,157 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import ArticulationCfg, AssetBaseCfg +from omni.isaac.lab.envs import ManagerBasedRLEnvCfg +from omni.isaac.lab.managers import ObservationGroupCfg as ObsGroup +from omni.isaac.lab.managers import ObservationTermCfg as ObsTerm +from omni.isaac.lab.managers import SceneEntityCfg +from omni.isaac.lab.managers import TerminationTermCfg as DoneTerm +from omni.isaac.lab.scene import InteractiveSceneCfg +from omni.isaac.lab.sensors import CameraCfg +from omni.isaac.lab.sensors.frame_transformer.frame_transformer_cfg import FrameTransformerCfg +from omni.isaac.lab.sim.spawners.from_files.from_files_cfg import GroundPlaneCfg, UsdFileCfg +from omni.isaac.lab.utils import configclass +from omni.isaac.lab.utils.assets import ISAAC_NUCLEUS_DIR + +from . import mdp + + +## +# Scene definition +## +@configclass +class ObjectTableSceneCfg(InteractiveSceneCfg): + """Configuration for the lift scene with a robot and a object. + This is the abstract base implementation, the exact scene is defined in the derived classes + which need to set the target object, robot and end-effector frames + """ + + # robots: will be populated by agent env cfg + robot: ArticulationCfg = MISSING + # end-effector sensor: will be populated by agent env cfg + ee_frame: FrameTransformerCfg = MISSING + + # Cameras + wrist_cam: CameraCfg = MISSING + table_cam: CameraCfg = MISSING + + # Table + table = AssetBaseCfg( + prim_path="{ENV_REGEX_NS}/Table", + init_state=AssetBaseCfg.InitialStateCfg(pos=[0.5, 0, 0], rot=[0.707, 0, 0, 0.707]), + spawn=UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd"), + ) + + # plane + plane = AssetBaseCfg( + prim_path="/World/GroundPlane", + init_state=AssetBaseCfg.InitialStateCfg(pos=[0, 0, -1.05]), + spawn=GroundPlaneCfg(), + ) + + # lights + light = AssetBaseCfg( + prim_path="/World/light", + spawn=sim_utils.DomeLightCfg(color=(0.75, 0.75, 0.75), intensity=3000.0), + ) + + +## +# MDP settings +## +@configclass +class ActionsCfg: + """Action specifications for the MDP.""" + + # will be set by agent env cfg + arm_action: mdp.JointPositionActionCfg = MISSING + gripper_action: mdp.BinaryJointPositionActionCfg = MISSING + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group with state values.""" + + actions = ObsTerm(func=mdp.last_action) + joint_pos = ObsTerm(func=mdp.joint_pos_rel) + joint_vel = ObsTerm(func=mdp.joint_vel_rel) + object = ObsTerm(func=mdp.instance_randomize_object_obs) + cube_positions = ObsTerm(func=mdp.instance_randomize_cube_positions_in_world_frame) + cube_orientations = ObsTerm(func=mdp.instance_randomize_cube_orientations_in_world_frame) + eef_pos = ObsTerm(func=mdp.ee_frame_pos) + eef_quat = ObsTerm(func=mdp.ee_frame_quat) + gripper_pos = ObsTerm(func=mdp.gripper_pos) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = False + + @configclass + class RGBCameraPolicyCfg(ObsGroup): + """Observations for policy group with RGB images.""" + + table_cam = ObsTerm( + func=mdp.image, params={"sensor_cfg": SceneEntityCfg("table_cam"), "data_type": "rgb", "normalize": False} + ) + wrist_cam = ObsTerm( + func=mdp.image, params={"sensor_cfg": SceneEntityCfg("wrist_cam"), "data_type": "rgb", "normalize": False} + ) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = False + + # observation groups + policy: PolicyCfg = PolicyCfg() + rgb_camera: RGBCameraPolicyCfg = RGBCameraPolicyCfg() + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + + +@configclass +class StackInstanceRandomizeEnvCfg(ManagerBasedRLEnvCfg): + """Configuration for the stacking environment.""" + + # Scene settings + scene: ObjectTableSceneCfg = ObjectTableSceneCfg(num_envs=4096, env_spacing=2.5, replicate_physics=False) + # Basic settings + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + # MDP settings + terminations: TerminationsCfg = TerminationsCfg() + + # Unused managers + commands = None + rewards = None + events = None + curriculum = None + + def __post_init__(self): + """Post initialization.""" + # general settings + self.decimation = 5 + self.episode_length_s = 30.0 + # simulation settings + self.sim.dt = 0.01 # 100Hz + self.sim.render_interval = self.decimation + + self.sim.physx.bounce_threshold_velocity = 0.2 + self.sim.physx.bounce_threshold_velocity = 0.01 + self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 + self.sim.physx.gpu_total_aggregate_pairs_capacity = 16 * 1024 + self.sim.physx.friction_correlation_distance = 0.00625 diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/navigation/config/anymal_c/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/navigation/config/anymal_c/__init__.py index 74ef1ccace..31e7989bc8 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/navigation/config/anymal_c/__init__.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/navigation/config/anymal_c/__init__.py @@ -5,7 +5,7 @@ import gymnasium as gym -from . import agents, navigation_env_cfg +from . import agents ## # Register Gym environments. @@ -16,7 +16,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": navigation_env_cfg.NavigationEnvCfg, + "env_cfg_entry_point": f"{__name__}.navigation_env_cfg:NavigationEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:NavigationEnvPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, @@ -27,7 +27,7 @@ entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": navigation_env_cfg.NavigationEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.navigation_env_cfg:NavigationEnvCfg_PLAY", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:NavigationEnvPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", }, 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/omni/isaac/lab_tasks/utils/data_collector/robomimic_data_collector.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/utils/data_collector/robomimic_data_collector.py index 5ef34f3c34..e0bab3b454 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/utils/data_collector/robomimic_data_collector.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/utils/data_collector/robomimic_data_collector.py @@ -15,7 +15,7 @@ import torch from collections.abc import Iterable -import carb +import omni.log class RobomimicDataCollector: @@ -148,10 +148,10 @@ def add(self, key: str, value: np.ndarray | torch.Tensor): """ # check if data should be recorded if self._is_first_interaction: - carb.log_warn("Please call reset before adding new data. Calling reset...") + omni.log.warn("Please call reset before adding new data. Calling reset...") self.reset() if self._is_stop: - carb.log_warn(f"Desired number of demonstrations collected: {self._demo_count} >= {self._num_demos}.") + omni.log.warn(f"Desired number of demonstrations collected: {self._demo_count} >= {self._num_demos}.") return # check datatype if isinstance(value, torch.Tensor): @@ -192,7 +192,7 @@ def flush(self, env_ids: Iterable[int] = (0,)): """ # check that data is being recorded if self._h5_file_stream is None or self._h5_data_group is None: - carb.log_error("No file stream has been opened. Please call reset before flushing data.") + omni.log.error("No file stream has been opened. Please call reset before flushing data.") return # iterate over each environment and add their data diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/utils/hydra.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/utils/hydra.py index 1522e9f25e..5d6a8b7c61 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/utils/hydra.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/utils/hydra.py @@ -17,6 +17,7 @@ raise ImportError("Hydra is not installed. Please install it by running 'pip install hydra-core'.") from omni.isaac.lab.envs import DirectRLEnvCfg, ManagerBasedRLEnvCfg +from omni.isaac.lab.envs.utils.spaces import replace_env_cfg_spaces_with_strings, replace_strings_with_env_cfg_spaces from omni.isaac.lab.utils import replace_slices_with_strings, replace_strings_with_slices from omni.isaac.lab_tasks.utils.parse_cfg import load_cfg_from_registry @@ -40,6 +41,9 @@ def register_task_to_hydra( # load the configurations env_cfg = load_cfg_from_registry(task_name, "env_cfg_entry_point") agent_cfg = load_cfg_from_registry(task_name, agent_cfg_entry_point) + # replace gymnasium spaces with strings because OmegaConf does not support them. + # this must be done before converting the env configs to dictionary to avoid internal reinterpretations + replace_env_cfg_spaces_with_strings(env_cfg) # convert the configs to dictionary env_cfg_dict = env_cfg.to_dict() if isinstance(agent_cfg, dict): @@ -83,6 +87,10 @@ def hydra_main(hydra_env_cfg: DictConfig, env_cfg=env_cfg, agent_cfg=agent_cfg): hydra_env_cfg = replace_strings_with_slices(hydra_env_cfg) # update the configs with the Hydra command line arguments env_cfg.from_dict(hydra_env_cfg["env"]) + # replace strings that represent gymnasium spaces because OmegaConf does not support them. + # this must be done after converting the env configs from dictionary to avoid internal reinterpretations + replace_strings_with_env_cfg_spaces(env_cfg) + # get agent configs if isinstance(agent_cfg, dict): agent_cfg = hydra_env_cfg["agent"] else: diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/vecenv_wrapper.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/vecenv_wrapper.py index 0badd08c31..0dedef9ef0 100644 --- a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/vecenv_wrapper.py +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/vecenv_wrapper.py @@ -70,19 +70,19 @@ def __init__(self, env: ManagerBasedRLEnv | DirectRLEnv): if hasattr(self.unwrapped, "action_manager"): self.num_actions = self.unwrapped.action_manager.total_action_dim else: - self.num_actions = self.unwrapped.num_actions + self.num_actions = gym.spaces.flatdim(self.unwrapped.single_action_space) if hasattr(self.unwrapped, "observation_manager"): self.num_obs = self.unwrapped.observation_manager.group_obs_dim["policy"][0] else: - self.num_obs = self.unwrapped.num_observations + self.num_obs = gym.spaces.flatdim(self.unwrapped.single_observation_space["policy"]) # -- privileged observations if ( hasattr(self.unwrapped, "observation_manager") and "critic" in self.unwrapped.observation_manager.group_obs_dim ): self.num_privileged_obs = self.unwrapped.observation_manager.group_obs_dim["critic"][0] - elif hasattr(self.unwrapped, "num_states"): - self.num_privileged_obs = self.unwrapped.num_states + elif hasattr(self.unwrapped, "num_states") and "critic" in self.unwrapped.single_observation_space: + self.num_privileged_obs = gym.spaces.flatdim(self.unwrapped.single_observation_space["critic"]) else: self.num_privileged_obs = 0 # reset at the start since the RSL-RL runner does not call reset 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 9e92e26156..8cd41fcd86 100644 --- a/source/extensions/omni.isaac.lab_tasks/test/test_environments.py +++ b/source/extensions/omni.isaac.lab_tasks/test/test_environments.py @@ -22,6 +22,7 @@ import omni.usd from omni.isaac.lab.envs import ManagerBasedRLEnvCfg +from omni.isaac.lab.envs.utils.spaces import sample_space import omni.isaac.lab_tasks # noqa: F401 from omni.isaac.lab_tasks.utils.parse_cfg import parse_env_cfg @@ -87,16 +88,24 @@ def _check_random_actions(self, task_name: str, device: str, num_envs: int, num_ """Run random actions and check environments returned signals are valid.""" # create a new stage omni.usd.get_context().new_stage() - # parse configuration - env_cfg: ManagerBasedRLEnvCfg = parse_env_cfg(task_name, device=device, num_envs=num_envs) - - # skip test if the environment is a multi-agent task - if hasattr(env_cfg, "possible_agents"): - print(f"[INFO]: Skipping {task_name} as it is a multi-agent task") - return - - # create environment - env = gym.make(task_name, cfg=env_cfg) + try: + # parse configuration + env_cfg: ManagerBasedRLEnvCfg = parse_env_cfg(task_name, device=device, num_envs=num_envs) + + # skip test if the environment is a multi-agent task + if hasattr(env_cfg, "possible_agents"): + print(f"[INFO]: Skipping {task_name} as it is a multi-agent task") + return + + # 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 @@ -108,12 +117,14 @@ def _check_random_actions(self, task_name: str, device: str, num_envs: int, num_ # simulate environment for num_steps steps with torch.inference_mode(): for _ in range(num_steps): - # sample actions from -1 to 1 - actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1 + # sample actions according to the defined space + actions = sample_space( + env.unwrapped.single_action_space, device=env.unwrapped.device, batch_size=num_envs + ) # apply actions transition = env.step(actions) # check signals - for data in transition: + for data in transition[:-1]: # exclude info self.assertTrue(self._check_valid_tensor(data), msg=f"Invalid data: {data}") # close the environment @@ -131,14 +142,10 @@ def _check_valid_tensor(data: torch.Tensor | dict) -> bool: """ if isinstance(data, torch.Tensor): return not torch.any(torch.isnan(data)) + elif isinstance(data, (tuple, list)): + return all(TestEnvironments._check_valid_tensor(value) for value in data) elif isinstance(data, dict): - valid_tensor = True - for value in data.values(): - if isinstance(value, dict): - valid_tensor &= TestEnvironments._check_valid_tensor(value) - elif isinstance(value, torch.Tensor): - valid_tensor &= not torch.any(torch.isnan(value)) - return valid_tensor + return all(TestEnvironments._check_valid_tensor(value) for value in data.values()) else: raise ValueError(f"Input data of invalid type: {type(data)}.") 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 19fcd88936..441bdb14f7 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 @@ -21,6 +21,7 @@ import omni.usd from omni.isaac.lab.envs import DirectMARLEnv, DirectMARLEnvCfg +from omni.isaac.lab.envs.utils.spaces import sample_space import omni.isaac.lab_tasks # noqa: F401 from omni.isaac.lab_tasks.utils.parse_cfg import parse_env_cfg @@ -83,19 +84,28 @@ def _check_random_actions(self, task_name: str, device: str, num_envs: int, num_ """Run random actions and check environments return valid signals.""" # create a new stage omni.usd.get_context().new_stage() - # parse configuration - env_cfg: DirectMARLEnvCfg = parse_env_cfg(task_name, device=device, num_envs=num_envs) + try: + # parse configuration + env_cfg: DirectMARLEnvCfg = parse_env_cfg(task_name, device=device, num_envs=num_envs) + + # skip test if the environment is not a multi-agent task + if not hasattr(env_cfg, "possible_agents"): + print(f"[INFO]: Skipping {task_name} as it is not a multi-agent task") + return + + # create environment + env: DirectMARLEnv = 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}") - # skip test if the environment is not a multi-agent task - if not hasattr(env_cfg, "possible_agents"): - print(f"[INFO]: Skipping {task_name} as it is not a multi-agent task") - return - - # create environment - env: DirectMARLEnv = gym.make(task_name, cfg=env_cfg) # this flag is necessary to prevent a bug where the simulation gets stuck randomly when running the # test on many environments. - env.sim.set_setting("/physics/cooking/ujitsoCollisionCooking", False) + env.unwrapped.sim.set_setting("/physics/cooking/ujitsoCollisionCooking", False) # reset environment obs, _ = env.reset() @@ -104,9 +114,11 @@ def _check_random_actions(self, task_name: str, device: str, num_envs: int, num_ # simulate environment for num_steps steps with torch.inference_mode(): for _ in range(num_steps): - # sample actions from -1 to 1 + # sample actions according to the defined space actions = { - agent: 2 * torch.rand(env.action_space(agent).shape, device=env.unwrapped.device) - 1 + agent: sample_space( + env.unwrapped.action_spaces[agent], device=env.unwrapped.device, batch_size=num_envs + ) for agent in env.unwrapped.possible_agents } # apply actions @@ -131,14 +143,10 @@ def _check_valid_tensor(data: torch.Tensor | dict) -> bool: """ if isinstance(data, torch.Tensor): return not torch.any(torch.isnan(data)) + elif isinstance(data, (tuple, list)): + return all(TestEnvironments._check_valid_tensor(value) for value in data) elif isinstance(data, dict): - valid_tensor = True - for value in data.values(): - if isinstance(value, dict): - valid_tensor &= TestEnvironments._check_valid_tensor(value) - elif isinstance(value, torch.Tensor): - valid_tensor &= not torch.any(torch.isnan(value)) - return valid_tensor + return all(TestEnvironments._check_valid_tensor(value) for value in data.values()) else: raise ValueError(f"Input data of invalid type: {type(data)}.") 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 e97ff5929e..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 @@ -20,6 +20,8 @@ import omni.usd +from omni.isaac.lab.envs import DirectMARLEnv, multi_agent_to_single_agent + import omni.isaac.lab_tasks # noqa: F401 from omni.isaac.lab_tasks.utils.parse_cfg import load_cfg_from_registry, parse_env_cfg from omni.isaac.lab_tasks.utils.wrappers.rl_games import RlGamesVecEnvWrapper @@ -55,13 +57,24 @@ def test_random_actions(self): print(f">>> Running test for environment: {task_name}") # create a new stage omni.usd.get_context().new_stage() - # parse configuration - env_cfg = parse_env_cfg(task_name, device=self.device, num_envs=self.num_envs) - agent_cfg = load_cfg_from_registry(task_name, "rl_games_cfg_entry_point") # noqa: F841 - # create environment - env = gym.make(task_name, cfg=env_cfg) - # wrap environment - env = RlGamesVecEnvWrapper(env, "cuda:0", 100, 100) + try: + # parse configuration + env_cfg = parse_env_cfg(task_name, device=self.device, num_envs=self.num_envs) + agent_cfg = load_cfg_from_registry(task_name, "rl_games_cfg_entry_point") # noqa: F841 + # create environment + env = gym.make(task_name, cfg=env_cfg) + # convert to single-agent instance if required by the RL algorithm + if isinstance(env.unwrapped, DirectMARLEnv): + env = multi_agent_to_single_agent(env) + # wrap environment + env = RlGamesVecEnvWrapper(env, "cuda:0", 100, 100) + 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}") # reset environment obs = env.reset() 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 154171e4cd..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 @@ -20,6 +20,8 @@ import omni.usd +from omni.isaac.lab.envs import DirectMARLEnv, multi_agent_to_single_agent + import omni.isaac.lab_tasks # noqa: F401 from omni.isaac.lab_tasks.utils.parse_cfg import load_cfg_from_registry, parse_env_cfg from omni.isaac.lab_tasks.utils.wrappers.rsl_rl import RslRlVecEnvWrapper @@ -55,13 +57,24 @@ def test_random_actions(self): print(f">>> Running test for environment: {task_name}") # create a new stage omni.usd.get_context().new_stage() - # parse configuration - env_cfg = parse_env_cfg(task_name, device=self.device, num_envs=self.num_envs) - agent_cfg = load_cfg_from_registry(task_name, "rsl_rl_cfg_entry_point") # noqa: F841 - # create environment - env = gym.make(task_name, cfg=env_cfg) - # wrap environment - env = RslRlVecEnvWrapper(env) + try: + # parse configuration + env_cfg = parse_env_cfg(task_name, device=self.device, num_envs=self.num_envs) + agent_cfg = load_cfg_from_registry(task_name, "rsl_rl_cfg_entry_point") # noqa: F841 + # create environment + env = gym.make(task_name, cfg=env_cfg) + # convert to single-agent instance if required by the RL algorithm + if isinstance(env.unwrapped, DirectMARLEnv): + env = multi_agent_to_single_agent(env) + # wrap environment + env = RslRlVecEnvWrapper(env) + 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}") # reset environment obs, extras = env.reset() @@ -69,9 +82,9 @@ def test_random_actions(self): self.assertTrue(self._check_valid_tensor(obs)) self.assertTrue(self._check_valid_tensor(extras)) - # simulate environment for 1000 steps + # simulate environment for 100 steps with torch.inference_mode(): - for _ in range(1000): + for _ in range(100): # sample actions from -1 to 1 actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1 # apply actions 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 afee3a8f04..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 @@ -21,6 +21,8 @@ import omni.usd +from omni.isaac.lab.envs import DirectMARLEnv, multi_agent_to_single_agent + import omni.isaac.lab_tasks # noqa: F401 from omni.isaac.lab_tasks.utils.parse_cfg import load_cfg_from_registry, parse_env_cfg from omni.isaac.lab_tasks.utils.wrappers.sb3 import Sb3VecEnvWrapper @@ -56,22 +58,33 @@ def test_random_actions(self): print(f">>> Running test for environment: {task_name}") # create a new stage omni.usd.get_context().new_stage() - # parse configuration - env_cfg = parse_env_cfg(task_name, device=self.device, num_envs=self.num_envs) - agent_cfg = load_cfg_from_registry(task_name, "sb3_cfg_entry_point") # noqa: F841 - # create environment - env = gym.make(task_name, cfg=env_cfg) - # wrap environment - env = Sb3VecEnvWrapper(env) + try: + # parse configuration + env_cfg = parse_env_cfg(task_name, device=self.device, num_envs=self.num_envs) + agent_cfg = load_cfg_from_registry(task_name, "sb3_cfg_entry_point") # noqa: F841 + # create environment + env = gym.make(task_name, cfg=env_cfg) + # convert to single-agent instance if required by the RL algorithm + if isinstance(env.unwrapped, DirectMARLEnv): + env = multi_agent_to_single_agent(env) + # wrap environment + env = Sb3VecEnvWrapper(env) + 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}") # reset environment obs = env.reset() # check signal self.assertTrue(self._check_valid_array(obs)) - # simulate environment for 1000 steps + # simulate environment for 100 steps with torch.inference_mode(): - for _ in range(1000): + for _ in range(100): # sample actions from -1 to 1 actions = 2 * np.random.rand(env.num_envs, *env.action_space.shape) - 1 # apply actions 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 90ae6eb3aa..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 @@ -57,25 +57,32 @@ def test_random_actions(self): print(f">>> Running test for environment: {task_name}") # create a new stage omni.usd.get_context().new_stage() - # parse configuration - env_cfg = parse_env_cfg(task_name, device=self.device, num_envs=self.num_envs) - agent_cfg = load_cfg_from_registry(task_name, "skrl_cfg_entry_point") # noqa: F841 - # create environment - env = gym.make(task_name, cfg=env_cfg) - if isinstance(env.unwrapped, DirectMARLEnv): - env = multi_agent_to_single_agent(env) - # wrap environment - env = SkrlVecEnvWrapper(env) - + try: + # parse configuration + env_cfg = parse_env_cfg(task_name, device=self.device, num_envs=self.num_envs) + agent_cfg = load_cfg_from_registry(task_name, "skrl_cfg_entry_point") # noqa: F841 + # create environment + env = gym.make(task_name, cfg=env_cfg) + if isinstance(env.unwrapped, DirectMARLEnv): + env = multi_agent_to_single_agent(env) + # wrap environment + env = SkrlVecEnvWrapper(env) + 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}") # reset environment obs, extras = env.reset() # check signal self.assertTrue(self._check_valid_tensor(obs)) self.assertTrue(self._check_valid_tensor(extras)) - # simulate environment for 1000 steps + # simulate environment for 100 steps with torch.inference_mode(): - for _ in range(10): + for _ in range(100): # sample actions from -1 to 1 actions = ( 2 * torch.rand(self.num_envs, *env.action_space.shape, device=env.unwrapped.device) - 1 diff --git a/source/standalone/benchmarks/benchmark_load_robot.py b/source/standalone/benchmarks/benchmark_load_robot.py new file mode 100644 index 0000000000..3ac0345e37 --- /dev/null +++ b/source/standalone/benchmarks/benchmark_load_robot.py @@ -0,0 +1,175 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Script to benchmark loading multiple copies of a robot. + +.. code-block python + + ./isaaclab.sh -p source/standalone/benchmarks/benchmark_load_robot.py --num_envs 2048 --robot g1 --headless + +""" + +"""Launch Isaac Sim Simulator first.""" + +import argparse +import time + +from omni.isaac.lab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Benchmark loading different robots.") +parser.add_argument("--num_envs", type=int, default=32, help="Number of robots to simulate.") +parser.add_argument( + "--robot", + type=str, + choices=["anymal_d", "h1", "g1"], + default="h1", + help="Choose which robot to load: anymal_d, h1, or g1.", +) +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli, _ = parser.parse_known_args() + +# Start the timer for app start +app_start_time_begin = time.perf_counter_ns() + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +# End the timer for app start +app_start_time_end = time.perf_counter_ns() + +print(f"[INFO]: App start time: {(app_start_time_end - app_start_time_begin) / 1e6:.2f} ms") + +"""Rest everything follows.""" + +# Start the timer for imports +imports_time_begin = time.perf_counter_ns() + +import torch + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import ArticulationCfg, AssetBaseCfg +from omni.isaac.lab.scene import InteractiveScene, InteractiveSceneCfg +from omni.isaac.lab.sim import SimulationContext +from omni.isaac.lab.utils import configclass + +## +# Pre-defined configs +## +from omni.isaac.lab_assets import ANYMAL_D_CFG, G1_MINIMAL_CFG, H1_MINIMAL_CFG # isort:skip + + +# Stop the timer for imports +imports_time_end = time.perf_counter_ns() + +print(f"[INFO]: Imports time: {(imports_time_end - imports_time_begin) / 1e6:.2f} ms") + + +@configclass +class RobotSceneCfg(InteractiveSceneCfg): + """Configuration for a simple scene with a robot.""" + + # 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)) + ) + + # articulation + if args_cli.robot == "h1": + robot: ArticulationCfg = H1_MINIMAL_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + elif args_cli.robot == "g1": + robot: ArticulationCfg = G1_MINIMAL_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + elif args_cli.robot == "anymal_d": + robot: ArticulationCfg = ANYMAL_D_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + else: + raise ValueError(f"Unsupported robot type: {args_cli.robot}.") + + +def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): + """Runs the simulation loop.""" + # Extract scene entities + # note: we only do this here for readability. + robot = scene["robot"] + # Define simulation stepping + sim_dt = sim.get_physics_dt() + + # Start the timer for creating the scene + step_time_begin = time.perf_counter_ns() + num_steps = 2000 + + # Simulation loop + for count in range(num_steps): + # Reset + if count % 500 == 0: + # reset the scene entities + # root state + # we offset the root state by the origin since the states are written in simulation world frame + # if this is not done, then the robots will be spawned at the (0, 0, 0) of the simulation world + root_state = robot.data.default_root_state.clone() + root_state[:, :3] += scene.env_origins + robot.write_root_state_to_sim(root_state) + # set joint positions with some noise + joint_pos, joint_vel = robot.data.default_joint_pos.clone(), robot.data.default_joint_vel.clone() + joint_pos += torch.rand_like(joint_pos) * 0.1 + robot.write_joint_state_to_sim(joint_pos, joint_vel) + # clear internal buffers + scene.reset() + # Apply random action + # -- generate random joint efforts + efforts = torch.randn_like(robot.data.joint_pos) * 5.0 + # -- apply action to the robot + robot.set_joint_effort_target(efforts) + # -- write data to sim + scene.write_data_to_sim() + # Perform step + sim.step() + # Update buffers + scene.update(sim_dt) + + # Stop the timer for reset + step_time_end = time.perf_counter_ns() + print(f"[INFO]: Per step time: {(step_time_end - step_time_begin) / num_steps / 1e6:.2f} ms") + + +def main(): + """Main function.""" + # Load kit helper + sim_cfg = sim_utils.SimulationCfg(device="cuda:0") + sim = SimulationContext(sim_cfg) + # Set main camera + sim.set_camera_view([2.5, 0.0, 4.0], [0.0, 0.0, 2.0]) + + # Start the timer for creating the scene + setup_time_begin = time.perf_counter_ns() + # Design scene + scene_cfg = RobotSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0) + scene = InteractiveScene(scene_cfg) + # Stop the timer for creating the scene + setup_time_end = time.perf_counter_ns() + print(f"[INFO]: Scene creation time: {(setup_time_end - setup_time_begin) / 1e6:.2f} ms") + + # Start the timer for reset + reset_time_begin = time.perf_counter_ns() + # Play the simulator + sim.reset() + # Stop the timer for reset + reset_time_end = time.perf_counter_ns() + print(f"[INFO]: Sim start time: {(reset_time_end - reset_time_begin) / 1e6:.2f} ms") + + # Run the simulator + run_simulator(sim, scene) + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/source/standalone/demos/multi_asset.py b/source/standalone/demos/multi_asset.py new file mode 100644 index 0000000000..2df770d13f --- /dev/null +++ b/source/standalone/demos/multi_asset.py @@ -0,0 +1,303 @@ +# 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=512, 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 ( + Articulation, + ArticulationCfg, + AssetBaseCfg, + RigidObject, + RigidObjectCfg, + RigidObjectCollection, + RigidObjectCollectionCfg, +) +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)), + ) + + # object collection + object_collection: RigidObjectCollectionCfg = RigidObjectCollectionCfg( + rigid_objects={ + "object_A": RigidObjectCfg( + prim_path="/World/envs/env_.*/Object_A", + spawn=sim_utils.SphereCfg( + radius=0.1, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2), + 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.5, 2.0)), + ), + "object_B": RigidObjectCfg( + prim_path="/World/envs/env_.*/Object_B", + spawn=sim_utils.CuboidCfg( + size=(0.1, 0.1, 0.1), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2), + 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.5, 2.0)), + ), + "object_C": RigidObjectCfg( + prim_path="/World/envs/env_.*/Object_C", + spawn=sim_utils.ConeCfg( + radius=0.1, + height=0.3, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2), + 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.5, 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: RigidObject = scene["object"] + rigid_object_collection: RigidObjectCollection = scene["object_collection"] + robot: Articulation = scene["robot"] + # Define simulation stepping + sim_dt = sim.get_physics_dt() + count = 0 + # Simulation loop + while simulation_app.is_running(): + # Reset + if count % 250 == 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) + # object collection + object_state = rigid_object_collection.data.default_object_state.clone() + object_state[..., :3] += scene.env_origins.unsqueeze(1) + rigid_object_collection.write_object_state_to_sim(object_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/source/standalone/demos/sensors/contact_sensor.py b/source/standalone/demos/sensors/contact_sensor.py new file mode 100644 index 0000000000..35de7fe667 --- /dev/null +++ b/source/standalone/demos/sensors/contact_sensor.py @@ -0,0 +1,176 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +from omni.isaac.lab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Tutorial on adding sensors on a robot.") +parser.add_argument("--num_envs", type=int, default=1, 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 torch + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import AssetBaseCfg, RigidObjectCfg +from omni.isaac.lab.scene import InteractiveScene, InteractiveSceneCfg +from omni.isaac.lab.sensors import ContactSensorCfg +from omni.isaac.lab.utils import configclass + +## +# Pre-defined configs +## +from omni.isaac.lab_assets.anymal import ANYMAL_C_CFG # isort: skip + + +@configclass +class ContactSensorSceneCfg(InteractiveSceneCfg): + """Design the scene with sensors on the robot.""" + + # 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)) + ) + + # robot + robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # Rigid Object + cube = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube", + spawn=sim_utils.CuboidCfg( + size=(0.5, 0.5, 0.1), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=100.0), + collision_props=sim_utils.CollisionPropertiesCfg(), + physics_material=sim_utils.RigidBodyMaterialCfg(static_friction=1.0), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.5, 0.5, 0.05)), + ) + + contact_forces_LF = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/LF_FOOT", + update_period=0.0, + history_length=6, + debug_vis=True, + filter_prim_paths_expr=["{ENV_REGEX_NS}/Cube"], + ) + + contact_forces_RF = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/RF_FOOT", + update_period=0.0, + history_length=6, + debug_vis=True, + filter_prim_paths_expr=["{ENV_REGEX_NS}/Cube"], + ) + + contact_forces_H = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/.*H_FOOT", + update_period=0.0, + history_length=6, + debug_vis=True, + ) + + +def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): + """Run the simulator.""" + # Define simulation stepping + sim_dt = sim.get_physics_dt() + sim_time = 0.0 + count = 0 + + # Simulate physics + while simulation_app.is_running(): + + if count % 500 == 0: + # reset counter + count = 0 + # reset the scene entities + # root state + # we offset the root state by the origin since the states are written in simulation world frame + # if this is not done, then the robots will be spawned at the (0, 0, 0) of the simulation world + root_state = scene["robot"].data.default_root_state.clone() + root_state[:, :3] += scene.env_origins + scene["robot"].write_root_state_to_sim(root_state) + # set joint positions with some noise + joint_pos, joint_vel = ( + scene["robot"].data.default_joint_pos.clone(), + scene["robot"].data.default_joint_vel.clone(), + ) + joint_pos += torch.rand_like(joint_pos) * 0.1 + scene["robot"].write_joint_state_to_sim(joint_pos, joint_vel) + # clear internal buffers + scene.reset() + print("[INFO]: Resetting robot state...") + # Apply default actions to the robot + # -- generate actions/commands + targets = scene["robot"].data.default_joint_pos + # -- apply action to the robot + scene["robot"].set_joint_position_target(targets) + # -- write data to sim + scene.write_data_to_sim() + # perform step + sim.step() + # update sim-time + sim_time += sim_dt + count += 1 + # update buffers + scene.update(sim_dt) + + # print information from the sensors + print("-------------------------------") + print(scene["contact_forces_LF"]) + print("Received force matrix of: ", scene["contact_forces_LF"].data.force_matrix_w) + print("Received contact force of: ", scene["contact_forces_LF"].data.net_forces_w) + print("-------------------------------") + print(scene["contact_forces_RF"]) + print("Received force matrix of: ", scene["contact_forces_RF"].data.force_matrix_w) + print("Received contact force of: ", scene["contact_forces_RF"].data.net_forces_w) + print("-------------------------------") + print(scene["contact_forces_H"]) + print("Received force matrix of: ", scene["contact_forces_H"].data.force_matrix_w) + print("Received contact force of: ", scene["contact_forces_H"].data.net_forces_w) + + +def main(): + """Main function.""" + + # Initialize the simulation context + sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device) + sim = sim_utils.SimulationContext(sim_cfg) + # Set main camera + sim.set_camera_view(eye=[3.5, 3.5, 3.5], target=[0.0, 0.0, 0.0]) + # design scene + scene_cfg = ContactSensorSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0) + scene = InteractiveScene(scene_cfg) + # 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 function + main() + # close sim app + simulation_app.close() diff --git a/source/standalone/demos/sensors/frame_transformer_sensor.py b/source/standalone/demos/sensors/frame_transformer_sensor.py new file mode 100644 index 0000000000..e66e59f500 --- /dev/null +++ b/source/standalone/demos/sensors/frame_transformer_sensor.py @@ -0,0 +1,170 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import argparse + +from omni.isaac.lab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Tutorial on adding sensors on a robot.") +parser.add_argument("--num_envs", type=int, default=1, 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 torch + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import AssetBaseCfg, RigidObjectCfg +from omni.isaac.lab.scene import InteractiveScene, InteractiveSceneCfg +from omni.isaac.lab.sensors import FrameTransformerCfg +from omni.isaac.lab.utils import configclass + +## +# Pre-defined configs +## +from omni.isaac.lab_assets.anymal import ANYMAL_C_CFG # isort: skip + + +@configclass +class FrameTransformerSensorSceneCfg(InteractiveSceneCfg): + """Design the scene with sensors on the robot.""" + + # 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)) + ) + + # robot + robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # Rigid Object + cube = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube", + spawn=sim_utils.CuboidCfg( + size=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=100.0), + collision_props=sim_utils.CollisionPropertiesCfg(), + physics_material=sim_utils.RigidBodyMaterialCfg(static_friction=1.0), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(5, 0, 0.5)), + ) + + specific_transforms = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/base", + target_frames=[ + FrameTransformerCfg.FrameCfg(prim_path="{ENV_REGEX_NS}/Robot/LF_FOOT"), + FrameTransformerCfg.FrameCfg(prim_path="{ENV_REGEX_NS}/Robot/RF_FOOT"), + ], + debug_vis=True, + ) + + cube_transform = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/base", + target_frames=[FrameTransformerCfg.FrameCfg(prim_path="{ENV_REGEX_NS}/Cube")], + debug_vis=False, + ) + + robot_transforms = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/base", + target_frames=[FrameTransformerCfg.FrameCfg(prim_path="{ENV_REGEX_NS}/Robot/.*")], + debug_vis=False, + ) + + +def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): + """Run the simulator.""" + # Define simulation stepping + sim_dt = sim.get_physics_dt() + sim_time = 0.0 + count = 0 + + # Simulate physics + while simulation_app.is_running(): + + if count % 500 == 0: + # reset counter + count = 0 + # reset the scene entities + # root state + # we offset the root state by the origin since the states are written in simulation world frame + # if this is not done, then the robots will be spawned at the (0, 0, 0) of the simulation world + root_state = scene["robot"].data.default_root_state.clone() + root_state[:, :3] += scene.env_origins + scene["robot"].write_root_state_to_sim(root_state) + # set joint positions with some noise + joint_pos, joint_vel = ( + scene["robot"].data.default_joint_pos.clone(), + scene["robot"].data.default_joint_vel.clone(), + ) + joint_pos += torch.rand_like(joint_pos) * 0.1 + scene["robot"].write_joint_state_to_sim(joint_pos, joint_vel) + # clear internal buffers + scene.reset() + print("[INFO]: Resetting robot state...") + # Apply default actions to the robot + # -- generate actions/commands + targets = scene["robot"].data.default_joint_pos + # -- apply action to the robot + scene["robot"].set_joint_position_target(targets) + # -- write data to sim + scene.write_data_to_sim() + # perform step + sim.step() + # update sim-time + sim_time += sim_dt + count += 1 + # update buffers + scene.update(sim_dt) + + # print information from the sensors + print("-------------------------------") + print(scene["specific_transforms"]) + print("relative transforms:", scene["specific_transforms"].data.target_pos_source) + print("relative orientations:", scene["specific_transforms"].data.target_quat_source) + print("-------------------------------") + print(scene["cube_transform"]) + print("relative transform:", scene["cube_transform"].data.target_pos_source) + print("-------------------------------") + print(scene["robot_transforms"]) + print("relative transforms:", scene["robot_transforms"].data.target_pos_source) + + +def main(): + """Main function.""" + + # Initialize the simulation context + sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device) + sim = sim_utils.SimulationContext(sim_cfg) + # Set main camera + sim.set_camera_view(eye=[3.5, 3.5, 3.5], target=[0.0, 0.0, 0.0]) + # design scene + scene_cfg = FrameTransformerSensorSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0) + scene = InteractiveScene(scene_cfg) + # 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 function + main() + # close sim app + simulation_app.close() diff --git a/source/standalone/demos/sensors/raycaster_sensor.py b/source/standalone/demos/sensors/raycaster_sensor.py new file mode 100644 index 0000000000..db7d74d3de --- /dev/null +++ b/source/standalone/demos/sensors/raycaster_sensor.py @@ -0,0 +1,160 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import argparse +import numpy as np + +from omni.isaac.lab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Tutorial on adding sensors on a robot.") +parser.add_argument("--num_envs", type=int, default=1, 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 torch + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import AssetBaseCfg +from omni.isaac.lab.scene import InteractiveScene, InteractiveSceneCfg +from omni.isaac.lab.sensors.ray_caster import RayCasterCfg, patterns +from omni.isaac.lab.utils import configclass +from omni.isaac.lab.utils.assets import ISAAC_NUCLEUS_DIR + +## +# Pre-defined configs +## +from omni.isaac.lab_assets.anymal import ANYMAL_C_CFG # isort: skip + + +@configclass +class RaycasterSensorSceneCfg(InteractiveSceneCfg): + """Design the scene with sensors on the robot.""" + + # ground plane + ground = AssetBaseCfg( + prim_path="/World/Ground", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Environments/Terrains/rough_plane.usd", + scale=(1, 1, 1), + ), + ) + + # lights + dome_light = AssetBaseCfg( + prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + ) + + # robot + robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + ray_caster = RayCasterCfg( + prim_path="{ENV_REGEX_NS}/Robot/base/lidar_cage", + update_period=1 / 60, + offset=RayCasterCfg.OffsetCfg(pos=(0, 0, 0.5)), + mesh_prim_paths=["/World/Ground"], + pattern_cfg=patterns.LidarPatternCfg( + channels=100, vertical_fov_range=[-90, 90], horizontal_fov_range=[-90, 90], horizontal_res=1.0 + ), + debug_vis=not args_cli.headless, + ) + + +def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): + """Run the simulator.""" + # Define simulation stepping + sim_dt = sim.get_physics_dt() + sim_time = 0.0 + count = 0 + + triggered = True + countdown = 42 + + # Simulate physics + while simulation_app.is_running(): + + if count % 500 == 0: + # reset counter + count = 0 + # reset the scene entities + # root state + # we offset the root state by the origin since the states are written in simulation world frame + # if this is not done, then the robots will be spawned at the (0, 0, 0) of the simulation world + root_state = scene["robot"].data.default_root_state.clone() + root_state[:, :3] += scene.env_origins + scene["robot"].write_root_state_to_sim(root_state) + # set joint positions with some noise + joint_pos, joint_vel = ( + scene["robot"].data.default_joint_pos.clone(), + scene["robot"].data.default_joint_vel.clone(), + ) + joint_pos += torch.rand_like(joint_pos) * 0.1 + scene["robot"].write_joint_state_to_sim(joint_pos, joint_vel) + # clear internal buffers + scene.reset() + print("[INFO]: Resetting robot state...") + # Apply default actions to the robot + # -- generate actions/commands + targets = scene["robot"].data.default_joint_pos + # -- apply action to the robot + scene["robot"].set_joint_position_target(targets) + # -- write data to sim + scene.write_data_to_sim() + # perform step + sim.step() + # update sim-time + sim_time += sim_dt + count += 1 + # update buffers + scene.update(sim_dt) + + # print information from the sensors + print("-------------------------------") + print(scene["ray_caster"]) + print("Ray cast hit results: ", scene["ray_caster"].data.ray_hits_w) + + if not triggered: + if countdown > 0: + countdown -= 1 + continue + data = scene["ray_caster"].data.ray_hits_w.cpu().numpy() + np.save("cast_data.npy", data) + triggered = True + else: + continue + + +def main(): + """Main function.""" + + # Initialize the simulation context + sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device) + sim = sim_utils.SimulationContext(sim_cfg) + # Set main camera + sim.set_camera_view(eye=[3.5, 3.5, 3.5], target=[0.0, 0.0, 0.0]) + # design scene + scene_cfg = RaycasterSensorSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0) + scene = InteractiveScene(scene_cfg) + # 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 function + main() + # close sim app + simulation_app.close() diff --git a/source/standalone/environments/state_machine/open_cabinet_sm.py b/source/standalone/environments/state_machine/open_cabinet_sm.py index f1e9b12fab..ad40653fca 100644 --- a/source/standalone/environments/state_machine/open_cabinet_sm.py +++ b/source/standalone/environments/state_machine/open_cabinet_sm.py @@ -40,10 +40,8 @@ import gymnasium as gym import torch -import traceback from collections.abc import Sequence -import carb import warp as wp from omni.isaac.lab.sensors import FrameTransformer @@ -313,13 +311,7 @@ def main(): if __name__ == "__main__": - try: - # run the main execution - main() - except Exception as err: - carb.log_error(err) - carb.log_error(traceback.format_exc()) - raise - finally: - # close sim app - simulation_app.close() + # run the main execution + main() + # close sim app + simulation_app.close() diff --git a/source/standalone/environments/teleoperation/teleop_se3_agent.py b/source/standalone/environments/teleoperation/teleop_se3_agent.py index ef3dc49e09..6361bd5f45 100644 --- a/source/standalone/environments/teleoperation/teleop_se3_agent.py +++ b/source/standalone/environments/teleoperation/teleop_se3_agent.py @@ -35,7 +35,7 @@ import gymnasium as gym import torch -import carb +import omni.log from omni.isaac.lab.devices import Se3Gamepad, Se3Keyboard, Se3SpaceMouse from omni.isaac.lab.managers import TerminationTermCfg as DoneTerm @@ -77,7 +77,7 @@ def main(): env = gym.make(args_cli.task, cfg=env_cfg) # check environment name (for reach , we don't allow the gripper) if "Reach" in args_cli.task: - carb.log_warn( + omni.log.warn( f"The environment '{args_cli.task}' does not support gripper control. The device command will be ignored." ) diff --git a/source/standalone/tools/convert_mesh.py b/source/standalone/tools/convert_mesh.py index 5cebf53d7f..5a88061067 100644 --- a/source/standalone/tools/convert_mesh.py +++ b/source/standalone/tools/convert_mesh.py @@ -106,10 +106,6 @@ def main(): if not os.path.isabs(dest_path): dest_path = os.path.abspath(dest_path) - print(dest_path) - print(os.path.dirname(dest_path)) - print(os.path.basename(dest_path)) - # Mass properties if args_cli.mass is not None: mass_props = schemas_cfg.MassPropertiesCfg(mass=args_cli.mass) diff --git a/source/standalone/tools/record_demos.py b/source/standalone/tools/record_demos.py new file mode 100644 index 0000000000..8c8e9bc208 --- /dev/null +++ b/source/standalone/tools/record_demos.py @@ -0,0 +1,187 @@ +# Copyright (c) 2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Script to record demonstrations with Isaac Lab environments using human teleoperation. + +This script allows users to record demonstrations operated by human teleoperation for a specified task. +The recorded demonstrations are stored as episodes in a hdf5 file. Users can specify the task, teleoperation +device, dataset directory, and environment stepping rate through command-line arguments. + +required arguments: + --task Name of the task. + +optional arguments: + -h, --help Show this help message and exit + --teleop_device Device for interacting with environment. (default: keyboard) + --dataset_file File path to export recorded demos. (default: "./datasets/dataset.hdf5") + --step_hz Environment stepping rate in Hz. (default: 30) +""" + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +from omni.isaac.lab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Record demonstrations for Isaac Lab environments.") +parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument("--teleop_device", type=str, default="keyboard", help="Device for interacting with environment.") +parser.add_argument( + "--dataset_file", type=str, default="./datasets/dataset.hdf5", help="File path to export recorded demos." +) +parser.add_argument("--step_hz", type=int, default=30, help="Environment stepping rate in Hz.") +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch the simulator +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import contextlib +import gymnasium as gym +import os +import time +import torch + +from omni.isaac.lab.devices import Se3Keyboard, Se3SpaceMouse +from omni.isaac.lab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg + +import omni.isaac.lab_tasks # noqa: F401 +from omni.isaac.lab_tasks.utils.parse_cfg import parse_env_cfg + + +class RateLimiter: + """Convenience class for enforcing rates in loops.""" + + def __init__(self, hz): + """ + Args: + hz (int): frequency to enforce + """ + self.hz = hz + self.last_time = time.time() + self.sleep_duration = 1.0 / hz + self.render_period = min(0.033, self.sleep_duration) + + def sleep(self, env): + """Attempt to sleep at the specified rate in hz.""" + next_wakeup_time = self.last_time + self.sleep_duration + while time.time() < next_wakeup_time: + time.sleep(self.render_period) + env.unwrapped.sim.render() + + self.last_time = self.last_time + self.sleep_duration + + # detect time jumping forwards (e.g. loop is too slow) + if self.last_time < time.time(): + while self.last_time < time.time(): + self.last_time += self.sleep_duration + + +def pre_process_actions(delta_pose: torch.Tensor, gripper_command: bool) -> torch.Tensor: + """Pre-process actions for the environment.""" + # compute actions based on environment + if "Reach" in args_cli.task: + # note: reach is the only one that uses a different action space + # compute actions + return delta_pose + else: + # resolve gripper command + gripper_vel = torch.zeros((delta_pose.shape[0], 1), dtype=torch.float, device=delta_pose.device) + gripper_vel[:] = -1 if gripper_command else 1 + # compute actions + return torch.concat([delta_pose, gripper_vel], dim=1) + + +def main(): + """Collect demonstrations from the environment using teleop interfaces.""" + + rate_limiter = RateLimiter(args_cli.step_hz) + + # get directory path and file name (without extension) from cli arguments + output_dir = os.path.dirname(args_cli.dataset_file) + output_file_name = os.path.splitext(os.path.basename(args_cli.dataset_file))[0] + + # create directory if it does not exist + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # parse configuration + env_cfg = parse_env_cfg(args_cli.task, device=args_cli.device, num_envs=1) + env_cfg.env_name = args_cli.task + + # modify configuration such that the environment runs indefinitely + # until goal is reached + env_cfg.terminations.time_out = None + + env_cfg.observations.policy.concatenate_terms = False + + env_cfg.recorders: ActionStateRecorderManagerCfg = ActionStateRecorderManagerCfg() + env_cfg.recorders.dataset_export_dir_path = output_dir + env_cfg.recorders.dataset_filename = output_file_name + + # create environment + env = gym.make(args_cli.task, cfg=env_cfg) + + # create controller + if args_cli.teleop_device.lower() == "keyboard": + teleop_interface = Se3Keyboard(pos_sensitivity=0.2, rot_sensitivity=0.5) + elif args_cli.teleop_device.lower() == "spacemouse": + teleop_interface = Se3SpaceMouse(pos_sensitivity=0.2, rot_sensitivity=0.5) + else: + raise ValueError(f"Invalid device interface '{args_cli.teleop_device}'. Supported: 'keyboard', 'spacemouse'.") + + # add teleoperation key for reset current recording instance + should_reset_recording_instance = False + + def reset_recording_instance(): + nonlocal should_reset_recording_instance + should_reset_recording_instance = True + + teleop_interface.add_callback("R", reset_recording_instance) + print(teleop_interface) + + # reset before starting + env.reset() + teleop_interface.reset() + + # simulate environment -- run everything in inference mode + with contextlib.suppress(KeyboardInterrupt) and torch.inference_mode(): + while True: + # get keyboard command + delta_pose, gripper_command = teleop_interface.advance() + # convert to torch + delta_pose = torch.tensor(delta_pose, dtype=torch.float, device=env.device).repeat(env.num_envs, 1) + # compute actions based on environment + actions = pre_process_actions(delta_pose, gripper_command) + + # perform action on environment + env.step(actions) + + if should_reset_recording_instance: + env.unwrapped.recorder_manager.reset() + env.reset() + should_reset_recording_instance = False + + # check that simulation is stopped or not + if env.unwrapped.sim.is_stopped(): + break + + rate_limiter.sleep(env.unwrapped) + + env.close() + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/source/standalone/tools/replay_demos.py b/source/standalone/tools/replay_demos.py new file mode 100644 index 0000000000..1c0a9b948c --- /dev/null +++ b/source/standalone/tools/replay_demos.py @@ -0,0 +1,229 @@ +# Copyright (c) 2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Script to replay demonstrations with Isaac Lab environments.""" + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +from omni.isaac.lab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Replay demonstrations in Isaac Lab environments.") +parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to replay episodes.") +parser.add_argument("--task", type=str, default=None, help="Force to use the specified task.") +parser.add_argument( + "--select_episodes", + type=int, + nargs="+", + default=[], + help="A list of episode indices to be replayed. Keep empty to replay all in the dataset file.", +) +parser.add_argument("--dataset_file", type=str, default="datasets/dataset.hdf5", help="Dataset file to be replayed.") +parser.add_argument( + "--validate_states", + action="store_true", + default=False, + help=( + "Validate if the states, if available, match between loaded from datasets and replayed. Only valid if" + " --num_envs is 1." + ), +) + +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() +# args_cli.headless = True + +# launch the simulator +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import contextlib +import gymnasium as gym +import os +import torch + +from omni.isaac.lab.devices import Se3Keyboard +from omni.isaac.lab.utils.datasets import EpisodeData, HDF5DatasetFileHandler + +import omni.isaac.lab_tasks # noqa: F401 +from omni.isaac.lab_tasks.utils.parse_cfg import parse_env_cfg + +is_paused = False + + +def play_cb(): + global is_paused + is_paused = False + + +def pause_cb(): + global is_paused + is_paused = True + + +def compare_states(state_from_dataset, runtime_state, runtime_env_index) -> (bool, str): + """Compare states from dataset and runtime. + + Args: + state_from_dataset: State from dataset. + runtime_state: State from runtime. + runtime_env_index: Index of the environment in the runtime states to be compared. + + Returns: + bool: True if states match, False otherwise. + str: Log message if states don't match. + """ + states_matched = True + output_log = "" + for asset_type in ["articulation", "rigid_object"]: + for asset_name in runtime_state[asset_type].keys(): + for state_name in runtime_state[asset_type][asset_name].keys(): + runtime_asset_state = runtime_state[asset_type][asset_name][state_name][runtime_env_index] + dataset_asset_state = state_from_dataset[asset_type][asset_name][state_name] + if len(dataset_asset_state) != len(runtime_asset_state): + raise ValueError(f"State shape of {state_name} for asset {asset_name} don't match") + for i in range(len(dataset_asset_state)): + if abs(dataset_asset_state[i] - runtime_asset_state[i]) > 0.01: + states_matched = False + output_log += f'\tState ["{asset_type}"]["{asset_name}"]["{state_name}"][{i}] don\'t match\r\n' + output_log += f"\t Dataset:\t{dataset_asset_state[i]}\r\n" + output_log += f"\t Runtime: \t{runtime_asset_state[i]}\r\n" + return states_matched, output_log + + +def main(): + """Replay episodes loaded from a file.""" + global is_paused + + # Load dataset + if not os.path.exists(args_cli.dataset_file): + raise FileNotFoundError(f"The dataset file {args_cli.dataset_file} does not exist.") + dataset_file_handler = HDF5DatasetFileHandler() + dataset_file_handler.open(args_cli.dataset_file) + env_name = dataset_file_handler.get_env_name() + episode_count = dataset_file_handler.get_num_episodes() + + if episode_count == 0: + print("No episodes found in the dataset.") + exit() + + episode_indices_to_replay = args_cli.select_episodes + if len(episode_indices_to_replay) == 0: + episode_indices_to_replay = list(range(episode_count)) + + if args_cli.task is not None: + env_name = args_cli.task + if env_name is None: + raise ValueError("Task/env name was not specified nor found in the dataset.") + + num_envs = args_cli.num_envs + + env_cfg = parse_env_cfg(env_name, device=args_cli.device, num_envs=num_envs) + + # Disable all recorders and terminations + env_cfg.recorders = {} + env_cfg.terminations = {} + + # create environment from loaded config + env = gym.make(env_name, cfg=env_cfg) + + teleop_interface = Se3Keyboard(pos_sensitivity=0.1, rot_sensitivity=0.1) + teleop_interface.add_callback("N", play_cb) + teleop_interface.add_callback("B", pause_cb) + print(teleop_interface) + + # Determine if state validation should be conducted + state_validation_enabled = False + if args_cli.validate_states and num_envs == 1: + state_validation_enabled = True + elif args_cli.validate_states and num_envs > 1: + print("Warning: State validation is only supported with a single environment. Skipping state validation.") + + # reset before starting + env.reset() + teleop_interface.reset() + + # simulate environment -- run everything in inference mode + episode_names = list(dataset_file_handler.get_episode_names()) + replayed_episode_count = 0 + with contextlib.suppress(KeyboardInterrupt) and torch.inference_mode(): + while simulation_app.is_running() and not simulation_app.is_exiting(): + env_episode_data_map = {index: EpisodeData() for index in range(num_envs)} + first_loop = True + has_next_action = True + while has_next_action: + # initialize actions with zeros so those without next action will not move + actions = torch.zeros(env.unwrapped.action_space.shape) + has_next_action = False + for env_id in range(num_envs): + env_next_action = env_episode_data_map[env_id].get_next_action() + if env_next_action is None: + next_episode_index = None + while episode_indices_to_replay: + next_episode_index = episode_indices_to_replay.pop(0) + if next_episode_index < episode_count: + break + next_episode_index = None + + if next_episode_index is not None: + replayed_episode_count += 1 + print(f"{replayed_episode_count :4}: Loading #{next_episode_index} episode to env_{env_id}") + episode_data = dataset_file_handler.load_episode( + episode_names[next_episode_index], env.unwrapped.device + ) + env_episode_data_map[env_id] = episode_data + # Set initial state for the new episode + initial_state = episode_data.get_initial_state() + env.unwrapped.reset_to( + initial_state, torch.tensor([env_id], device=env.unwrapped.device), is_relative=True + ) + # Get the first action for the new episode + env_next_action = env_episode_data_map[env_id].get_next_action() + has_next_action = True + else: + continue + else: + has_next_action = True + actions[env_id] = env_next_action + if first_loop: + first_loop = False + else: + while is_paused: + env.unwrapped.sim.render() + continue + env.step(actions) + + if state_validation_enabled: + state_from_dataset = env_episode_data_map[0].get_next_state() + if state_from_dataset is not None: + print( + f"Validating states at action-index: {env_episode_data_map[0].next_state_index - 1 :4}", + end="", + ) + current_runtime_state = env.unwrapped.scene.get_state(is_relative=True) + states_matched, comparison_log = compare_states(state_from_dataset, current_runtime_state, 0) + if states_matched: + print("\t- matched.") + else: + print("\t- mismatched.") + print(comparison_log) + break + # Close environment after replay in complete + plural_trailing_s = "s" if replayed_episode_count > 1 else "" + print(f"Finished replaying {replayed_episode_count} episode{plural_trailing_s}.") + env.close() + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/source/standalone/tutorials/00_sim/log_time.py b/source/standalone/tutorials/00_sim/log_time.py index e55c9f2a62..f47d96222a 100644 --- a/source/standalone/tutorials/00_sim/log_time.py +++ b/source/standalone/tutorials/00_sim/log_time.py @@ -40,12 +40,14 @@ def main(): """Main function.""" # Specify that the logs must be in logs/docker_tutorial - log_dir_path = os.path.join("logs", "docker_tutorial") + log_dir_path = os.path.join("logs") + if not os.path.isdir(log_dir_path): + os.mkdir(log_dir_path) # In the container, the absolute path will be # /workspace/isaaclab/logs/docker_tutorial, because # all python execution is done through /workspace/isaaclab/isaaclab.sh # and the calling process' path will be /workspace/isaaclab - log_dir_path = os.path.abspath(log_dir_path) + log_dir_path = os.path.abspath(os.path.join(log_dir_path, "docker_tutorial")) if not os.path.isdir(log_dir_path): os.mkdir(log_dir_path) print(f"[INFO] Logging experiment to directory: {log_dir_path}") diff --git a/source/standalone/tutorials/01_assets/run_deformable_object.py b/source/standalone/tutorials/01_assets/run_deformable_object.py index eccbfab06e..8a6177d1d4 100644 --- a/source/standalone/tutorials/01_assets/run_deformable_object.py +++ b/source/standalone/tutorials/01_assets/run_deformable_object.py @@ -109,7 +109,7 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, Deformab # write nodal state to simulation cube_object.write_nodal_state_to_sim(nodal_state) - # write kinematic target to nodal state and free all vertices + # Write the nodal state to the kinematic target and free all vertices nodal_kinematic_target[..., :3] = nodal_state[..., :3] nodal_kinematic_target[..., 3] = 1.0 cube_object.write_nodal_kinematic_target_to_sim(nodal_kinematic_target) diff --git a/source/standalone/tutorials/04_sensors/run_ray_caster_camera.py b/source/standalone/tutorials/04_sensors/run_ray_caster_camera.py index a54d9dc6f6..22958ec3a3 100644 --- a/source/standalone/tutorials/04_sensors/run_ray_caster_camera.py +++ b/source/standalone/tutorials/04_sensors/run_ray_caster_camera.py @@ -129,24 +129,19 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): # Extract camera data camera_index = 0 # note: BasicWriter only supports saving data in numpy format, so we need to convert the data to numpy. - if sim.backend == "torch": - # tensordict allows easy indexing of tensors in the dictionary - single_cam_data = convert_dict_to_backend(camera.data.output[camera_index], backend="numpy") - else: - # for numpy, we need to manually index the data - single_cam_data = dict() - for key, value in camera.data.output.items(): - single_cam_data[key] = value[camera_index] + single_cam_data = convert_dict_to_backend( + {k: v[camera_index] for k, v in camera.data.output.items()}, backend="numpy" + ) # Extract the other information single_cam_info = camera.data.info[camera_index] # Pack data back into replicator format to save them using its writer - rep_output = dict() + rep_output = {"annotators": {}} for key, data, info in zip(single_cam_data.keys(), single_cam_data.values(), single_cam_info.values()): if info is not None: - rep_output[key] = {"data": data, "info": info} + rep_output["annotators"][key] = {"render_product": {"data": data, **info}} else: - rep_output[key] = data + rep_output["annotators"][key] = {"render_product": {"data": data}} # Save images rep_output["trigger_outputs"] = {"on_time": camera.frame[camera_index]} rep_writer.write(rep_output) diff --git a/source/standalone/tutorials/04_sensors/run_usd_camera.py b/source/standalone/tutorials/04_sensors/run_usd_camera.py index 129a76f566..22d0871441 100644 --- a/source/standalone/tutorials/04_sensors/run_usd_camera.py +++ b/source/standalone/tutorials/04_sensors/run_usd_camera.py @@ -228,8 +228,9 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): if args_cli.save: # Save images from camera at camera_index # note: BasicWriter only supports saving data in numpy format, so we need to convert the data to numpy. - # tensordict allows easy indexing of tensors in the dictionary - single_cam_data = convert_dict_to_backend(camera.data.output[camera_index], backend="numpy") + single_cam_data = convert_dict_to_backend( + {k: v[camera_index] for k, v in camera.data.output.items()}, backend="numpy" + ) # Extract the other information single_cam_info = camera.data.info[camera_index] diff --git a/source/standalone/workflows/rsl_rl/train.py b/source/standalone/workflows/rsl_rl/train.py index f02e0a3c0f..6c73798315 100644 --- a/source/standalone/workflows/rsl_rl/train.py +++ b/source/standalone/workflows/rsl_rl/train.py @@ -99,6 +99,11 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) + + # save resume path before creating a new log_dir + if agent_cfg.resume: + resume_path = get_checkpoint_path(log_root_path, agent_cfg.load_run, agent_cfg.load_checkpoint) + # wrap for video recording if args_cli.video: video_kwargs = { @@ -122,10 +127,8 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen runner = OnPolicyRunner(env, agent_cfg.to_dict(), log_dir=log_dir, device=agent_cfg.device) # write git state to logs runner.add_git_repo_to_log(__file__) - # save resume path before creating a new log_dir + # load the checkpoint if agent_cfg.resume: - # get path to previous checkpoint - resume_path = get_checkpoint_path(log_root_path, agent_cfg.load_run, agent_cfg.load_checkpoint) print(f"[INFO]: Loading model checkpoint from: {resume_path}") # load previously trained model runner.load(resume_path) diff --git a/tools/per_test_timeouts.py b/tools/per_test_timeouts.py index e9fad0a293..4310c632c7 100644 --- a/tools/per_test_timeouts.py +++ b/tools/per_test_timeouts.py @@ -8,6 +8,8 @@ Any tests not listed here will use the default timeout. """ PER_TEST_TIMEOUTS = { + "test_articulation.py": 200, + "test_deformable_object.py": 200, "test_environments.py": 1200, # This test runs through all the environments for 100 steps each "test_environment_determinism.py": 200, # This test runs through many the environments for 100 steps each "test_env_rendering_logic.py": 300, diff --git a/tools/run_all_tests.py b/tools/run_all_tests.py index 733af27c02..bfed59e343 100644 --- a/tools/run_all_tests.py +++ b/tools/run_all_tests.py @@ -23,6 +23,7 @@ import argparse import logging import os +import re import subprocess import sys import time @@ -118,57 +119,9 @@ def test_all( # Set up logger logging.basicConfig(level=logging.INFO, format="%(message)s", handlers=logging_handlers) - # Discover all tests under current directory - all_test_paths = [str(path) for path in Path(test_dir).resolve().rglob("*test_*.py")] - skipped_test_paths = [] - test_paths = [] - # Check that all tests to skip are actually in the tests - for test_to_skip in tests_to_skip: - for test_path in all_test_paths: - if test_to_skip in test_path: - break - else: - raise ValueError(f"Test to skip '{test_to_skip}' not found in tests.") - - # Filter tests by extension - if extension is not None: - all_tests_in_selected_extension = [] - - for test_path in all_test_paths: - # Extract extension name from test path - extension_name = test_path[test_path.find("extensions") :].split("/")[1] - - # Skip tests that are not in the selected extension - if extension_name != extension: - continue - - all_tests_in_selected_extension.append(test_path) - - all_test_paths = all_tests_in_selected_extension - - # Remove tests to skip from the list of tests to run - if len(tests_to_skip) != 0: - for test_path in all_test_paths: - if any([test_to_skip in test_path for test_to_skip in tests_to_skip]): - skipped_test_paths.append(test_path) - else: - test_paths.append(test_path) - else: - test_paths = all_test_paths - - # Sort test paths so they're always in the same order - all_test_paths.sort() - test_paths.sort() - skipped_test_paths.sort() - - # Initialize all tests to have the same timeout - test_timeouts = {test_path: timeout for test_path in all_test_paths} - - # Overwrite timeouts for specific tests - for test_path_with_timeout, test_timeout in per_test_timeouts.items(): - for test_path in all_test_paths: - if test_path_with_timeout in test_path: - test_timeouts[test_path] = test_timeout + all_test_paths, test_paths, skipped_test_paths, test_timeouts = extract_tests_and_timeouts( + test_dir, extension, tests_to_skip, timeout, per_test_timeouts + ) # Print tests to be run logging.info("\n" + "=" * 60 + "\n") @@ -213,36 +166,31 @@ def test_all( except Exception as e: logging.error(f"Unexpected exception {e}. Please report this issue on the repository.") result = "FAILED" - stdout = str(e) - stderr = str(e) + stdout = None + stderr = None else: - # Should only get here if the process ran successfully, e.g. no exceptions were raised - # but we still check the returncode just in case - result = "PASSED" if completed_process.returncode == 0 else "FAILED" + result = "COMPLETED" stdout = completed_process.stdout stderr = completed_process.stderr after = time.time() time_elapsed = after - before - # Decode stdout and stderr and write to file and print to console if desired - if stdout is not None: - if isinstance(stdout, str): - stdout_str = stdout - else: - stdout_str = stdout.decode("utf-8") - else: - stdout_str = "" - if stderr is not None: - if isinstance(stderr, str): - stderr_str = stderr + + # Decode stdout and stderr + stdout = stdout.decode("utf-8") if stdout is not None else "" + stderr = stderr.decode("utf-8") if stderr is not None else "" + + if result == "COMPLETED": + # Check for success message in the output + success_pattern = r"Ran \d+ tests? in [\d.]+s\s+OK" + if re.search(success_pattern, stdout) or re.search(success_pattern, stderr): + result = "PASSED" else: - stderr_str = stderr.decode("utf-8") - else: - stderr_str = "" + result = "FAILED" # Write to log file - logging.info(stdout_str) - logging.info(stderr_str) + logging.info(stdout) + logging.info(stderr) logging.info(f"[INFO] Time elapsed: {time_elapsed:.2f} s") logging.info(f"[INFO] Result '{test_path}': {result}") # Collect results @@ -307,8 +255,89 @@ def test_all( return num_failing + num_timing_out == 0 +def extract_tests_and_timeouts( + test_dir: str, + extension: str | None = None, + tests_to_skip: list[str] = [], + timeout: float = DEFAULT_TIMEOUT, + per_test_timeouts: dict[str, float] = {}, +) -> tuple[list[str], list[str], list[str], dict[str, float]]: + """Extract all tests under the given directory or extension and their respective timeouts. + + Args: + test_dir: Path to the directory containing the tests. + extension: Run tests only for the given extension. Defaults to None, which means all extensions' + tests will be run. + tests_to_skip: List of tests to skip. + timeout: Timeout for each test in seconds. Defaults to DEFAULT_TIMEOUT. + per_test_timeouts: A dictionary of tests and their timeouts in seconds. Any tests not listed here will use the + timeout specified by `timeout`. Defaults to an empty dictionary. + + Returns: + A tuple containing the paths of all tests, tests to run, tests to skip, and their respective timeouts. + + Raises: + ValueError: If any test to skip is not found under the given `test_dir`. + """ + + # Discover all tests under current directory + all_test_paths = [str(path) for path in Path(test_dir).resolve().rglob("*test_*.py")] + skipped_test_paths = [] + test_paths = [] + # Check that all tests to skip are actually in the tests + for test_to_skip in tests_to_skip: + for test_path in all_test_paths: + if test_to_skip in test_path: + break + else: + raise ValueError(f"Test to skip '{test_to_skip}' not found in tests.") + + # Filter tests by extension + if extension is not None: + all_tests_in_selected_extension = [] + + for test_path in all_test_paths: + # Extract extension name from test path + extension_name = test_path[test_path.find("extensions") :].split("/")[1] + + # Skip tests that are not in the selected extension + if extension_name != extension: + continue + + all_tests_in_selected_extension.append(test_path) + + all_test_paths = all_tests_in_selected_extension + + # Remove tests to skip from the list of tests to run + if len(tests_to_skip) != 0: + for test_path in all_test_paths: + if any([test_to_skip in test_path for test_to_skip in tests_to_skip]): + skipped_test_paths.append(test_path) + else: + test_paths.append(test_path) + else: + test_paths = all_test_paths + + # Sort test paths so they're always in the same order + all_test_paths.sort() + test_paths.sort() + skipped_test_paths.sort() + + # Initialize all tests to have the same timeout + test_timeouts = {test_path: timeout for test_path in all_test_paths} + + # Overwrite timeouts for specific tests + for test_path_with_timeout, test_timeout in per_test_timeouts.items(): + for test_path in all_test_paths: + if test_path_with_timeout in test_path: + test_timeouts[test_path] = test_timeout + + return all_test_paths, test_paths, skipped_test_paths, test_timeouts + + def warm_start_app(): """Warm start the app to compile shaders before running the tests.""" + print("[INFO] Warm starting the simulation app before running tests.") before = time.time() # headless experience