diff --git a/.github/actions/docker-build/action.yml b/.github/actions/docker-build/action.yml index 51f0a90d..22c82ada 100644 --- a/.github/actions/docker-build/action.yml +++ b/.github/actions/docker-build/action.yml @@ -32,9 +32,9 @@ runs: shell: sh run: | # Only attempt NGC login if API key is available - if [ -n "${{ env.NGC_API_KEY }}" ]; then + if [ -n "$NGC_API_KEY" ]; then echo "Logging into NGC registry..." - docker login -u \$oauthtoken -p ${{ env.NGC_API_KEY }} nvcr.io + printf '%s' "$NGC_API_KEY" | docker login -u '$oauthtoken' --password-stdin nvcr.io echo "✅ Successfully logged into NGC registry" else echo "⚠️ NGC_API_KEY not available - skipping NGC login" @@ -58,7 +58,6 @@ runs: # Build Docker image docker buildx build --progress=plain --platform linux/amd64 \ - -t uw-lab-dev \ -t $image_tag \ --build-arg ISAACSIM_BASE_IMAGE_ARG="$isaacsim_base_image" \ --build-arg ISAACSIM_VERSION_ARG="$isaacsim_version" \ diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index df471c1d..7f5b7dac 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -59,8 +59,11 @@ runs: # Create reports directory mkdir -p "$reports_dir" - # Clean up any existing container - docker rm -f $container_name 2>/dev/null || true + # Preserve any existing container + if docker container inspect "$container_name" >/dev/null 2>&1; then + echo "Container already exists; refusing to replace it: $container_name" >&2 + return 1 + fi # Build Docker environment variables docker_env_vars="\ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index de9b797f..19ce8c1a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,8 +26,8 @@ permissions: env: NGC_API_KEY: ${{ secrets.NGC_API_KEY }} ISAACSIM_BASE_IMAGE: ${{ vars.ISAACSIM_BASE_IMAGE || 'nvcr.io/nvidia/isaac-sim' }} - ISAACSIM_BASE_VERSION: ${{ vars.ISAACSIM_BASE_VERSION || '5.1.0' }} - DOCKER_IMAGE_TAG: uw-lab-dev:${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}-${{ github.sha }} + ISAACSIM_BASE_VERSION: ${{ vars.ISAACSIM_BASE_VERSION || '6.1.0' }} + DOCKER_IMAGE_TAG: uw-lab-dev:${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }} jobs: test-uwlab-tasks: @@ -37,25 +37,15 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 lfs: true - - name: Remove previous uw-lab-dev images - run: | - IDS=$(docker image ls -q uw-lab-dev 2>/dev/null || true) - if [ -n "$IDS" ]; then - echo "Removing existing uw-lab-dev images: $IDS" - docker image rm -f $IDS || true - else - echo "No existing uw-lab-dev images to remove." - fi - - name: Build Docker Image uses: ./.github/actions/docker-build with: - image-tag: ${{ env.DOCKER_IMAGE_TAG }} + image-tag: ${{ env.DOCKER_IMAGE_TAG }}-${{ github.job }} isaacsim-base-image: ${{ env.ISAACSIM_BASE_IMAGE }} isaacsim-version: ${{ env.ISAACSIM_BASE_VERSION }} @@ -64,21 +54,21 @@ jobs: with: test-path: "tools" result-file: "uwlab-tasks-report.xml" - container-name: "uw-lab-tasks-test-$$" - image-tag: ${{ env.DOCKER_IMAGE_TAG }} + container-name: "uw-lab-tasks-test-${{ github.run_id }}-${{ github.run_attempt }}" + image-tag: ${{ env.DOCKER_IMAGE_TAG }}-${{ github.job }} pytest-options: "" filter-pattern: "uwlab_tasks" - name: Copy Test Results from UWLab Tasks Container run: | - CONTAINER_NAME="uw-lab-tasks-test-$$" + CONTAINER_NAME="uw-lab-tasks-test-${{ github.run_id }}-${{ github.run_attempt }}" if docker ps -a | grep -q $CONTAINER_NAME; then echo "Copying test results from UWLab Tasks container..." docker cp $CONTAINER_NAME:/workspace/uwlab/tests/uwlab-tasks-report.xml reports/ 2>/dev/null || echo "No test results to copy from UWLab Tasks container" fi - name: Upload UWLab Tasks Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() with: name: uwlab-tasks-test-results @@ -100,31 +90,25 @@ jobs: exit 1 fi + - name: Remove this job's image tag + if: always() + run: docker image rm "${{ env.DOCKER_IMAGE_TAG }}-${{ github.job }}" || true + test-general: runs-on: [self-hosted, gpu] timeout-minutes: 180 steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 lfs: true - - name: Remove previous uw-lab-dev images - run: | - IDS=$(docker image ls -q uw-lab-dev 2>/dev/null || true) - if [ -n "$IDS" ]; then - echo "Removing existing uw-lab-dev images: $IDS" - docker image rm -f $IDS || true - else - echo "No existing uw-lab-dev images to remove." - fi - - name: Build Docker Image uses: ./.github/actions/docker-build with: - image-tag: ${{ env.DOCKER_IMAGE_TAG }} + image-tag: ${{ env.DOCKER_IMAGE_TAG }}-${{ github.job }} isaacsim-base-image: ${{ env.ISAACSIM_BASE_IMAGE }} isaacsim-version: ${{ env.ISAACSIM_BASE_VERSION }} @@ -134,21 +118,21 @@ jobs: with: test-path: "tools" result-file: "general-tests-report.xml" - container-name: "uw-lab-general-test-$$" - image-tag: ${{ env.DOCKER_IMAGE_TAG }} + container-name: "uw-lab-general-test-${{ github.run_id }}-${{ github.run_attempt }}" + image-tag: ${{ env.DOCKER_IMAGE_TAG }}-${{ github.job }} pytest-options: "" filter-pattern: "not uwlab_tasks" - name: Copy Test Results from General Tests Container run: | - CONTAINER_NAME="uw-lab-general-test-$$" + CONTAINER_NAME="uw-lab-general-test-${{ github.run_id }}-${{ github.run_attempt }}" if docker ps -a | grep -q $CONTAINER_NAME; then echo "Copying test results from General Tests container..." docker cp $CONTAINER_NAME:/workspace/uwlab/tests/general-tests-report.xml reports/ 2>/dev/null || echo "No test results to copy from General Tests container" fi - name: Upload General Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() with: name: general-test-results @@ -170,6 +154,10 @@ jobs: exit 1 fi + - name: Remove this job's image tag + if: always() + run: docker image rm "${{ env.DOCKER_IMAGE_TAG }}-${{ github.job }}" || true + combine-results: needs: [test-uwlab-tasks, test-general] runs-on: [self-hosted, gpu] @@ -177,7 +165,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 lfs: false @@ -187,14 +175,14 @@ jobs: mkdir -p reports - name: Download Test Results - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: uwlab-tasks-test-results path: reports/ continue-on-error: true - name: Download General Test Results - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: general-test-results path: reports/ @@ -206,7 +194,7 @@ jobs: output-file: "reports/combined-results.xml" - name: Upload Combined Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() with: name: pr-${{ github.event.pull_request.number }}-combined-test-results @@ -217,7 +205,7 @@ jobs: - name: Comment on Test Results id: test-reporter if: github.event.pull_request.head.repo.full_name == github.repository - uses: EnricoMi/publish-unit-test-result-action@v2 + uses: EnricoMi/publish-unit-test-result-action@d0a4676d0e0b938bc201470d88276b7c74c712b3 # v2 with: files: "reports/combined-results.xml" check_name: "Tests Summary" @@ -231,7 +219,7 @@ jobs: - name: Report Test Results if: github.event.pull_request.head.repo.full_name == github.repository - uses: dorny/test-reporter@v1 + uses: dorny/test-reporter@d61b558e8df85cb60d09ca3e5b09653b4477cea7 # v1 with: name: UWLab Build and Test Results path: reports/combined-results.xml diff --git a/.github/workflows/license-check.yaml b/.github/workflows/license-check.yaml index b9ba98ab..c2f1cd3b 100644 --- a/.github/workflows/license-check.yaml +++ b/.github/workflows/license-check.yaml @@ -19,25 +19,31 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3 # - name: Install jq # run: sudo apt-get update && sudo apt-get install -y jq - - name: Clean up disk space - run: | - rm -rf /opt/hostedtoolcache - - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4 with: - python-version: '3.11' # Adjust as needed + python-version: '3.12' # Adjust as needed + + - name: Check pinned installer behavior + run: python tools/test_installer_pin.py + + - name: Create isolated license environment + run: | + ENV_PATH="$RUNNER_TEMP/uwlab-license-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + python -m venv "$ENV_PATH" + echo "$ENV_PATH/bin" >> "$GITHUB_PATH" + echo "VIRTUAL_ENV=$ENV_PATH" >> "$GITHUB_ENV" - name: Install dependencies using ./uwlab.sh -i run: | # first install isaac sim pip install --upgrade pip - pip install 'isaacsim[all,extscache]==${{ vars.ISAACSIM_BASE_VERSION || '5.0.0' }}' --extra-index-url https://pypi.nvidia.com + pip install 'isaacsim[all,extscache]==${{ vars.ISAACSIM_PIP_VERSION || '6.1.0.0' }}' --extra-index-url https://pypi.nvidia.com chmod +x ./uwlab.sh # Make sure the script is executable # install all lab dependencies ./uwlab.sh -i diff --git a/.greptile/rules.md b/.greptile/rules.md index 71ebd9e5..1e26f041 100644 --- a/.greptile/rules.md +++ b/.greptile/rules.md @@ -135,6 +135,9 @@ Every entry-point string must resolve; `-Play-v0` variants normally reuse the tr and a bullet under **Getting Started** in `README.md`. - Checkpoints and datasets go to the Hugging Face dataset `UW-Lab/uwlab-assets` (`Policies/` for checkpoints) via fork + PR, and the docs link to them. They are never committed here. +- Assets consumed by Isaac Lab 3.0 live on that repository's `isaaclab3` branch (quaternions in + `(x, y, z, w)`); `main` keeps the Isaac Lab 2.x files. Code pins a commit via + `uwlab_assets.UWLAB_CLOUD_ASSETS_REVISION`; bump it deliberately when publishing assets. - Heavy or research-only dependencies (diffusion_policy, robomimic, ...) are not added to core install requirements, so a default install stays light. Use an `extras_require` group (`EXTRAS_REQUIRE` in `source/uwlab_rl/setup.py` is the pattern), a git submodule, or install diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9aec2185..164f7780 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,8 +4,8 @@ UW Lab is a community maintained project. We wholeheartedly welcome contribution the framework more mature and useful for everyone. These may happen in forms of bug reports, feature requests, design proposals and more. -For general information on how to contribute see -. +For development setup and extension structure, see the +[developer guide](docs/source/overview/developer-guide/development.rst). --- diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 73998c5d..da649ccd 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -20,6 +20,7 @@ Guidelines for modifications: --- * Feng Yu +* Joshua Tran * Mateo Guaman Castro * Patrick Yin * Quanquan Peng diff --git a/README.md b/README.md index a97c857f..8bb11e7a 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ # UW Lab -[![IsaacSim](https://img.shields.io/badge/IsaacSim-5.1.0-silver.svg)](https://docs.isaacsim.omniverse.nvidia.com/latest/index.html) -[![Python](https://img.shields.io/badge/python-3.11-blue.svg)](https://docs.python.org/3/whatsnew/3.11.html) +[![IsaacSim](https://img.shields.io/badge/IsaacSim-6.1.0-silver.svg)](https://docs.isaacsim.omniverse.nvidia.com/latest/index.html) +[![Python](https://img.shields.io/badge/python-3.12-blue.svg)](https://docs.python.org/3/whatsnew/3.12.html) [![Linux platform](https://img.shields.io/badge/platform-linux--64-orange.svg)](https://releases.ubuntu.com/20.04/) [![Windows platform](https://img.shields.io/badge/platform-windows--64-orange.svg)](https://www.microsoft.com/en-us/) [![pre-commit](https://img.shields.io/github/actions/workflow/status/isaac-sim/IsaacLab/pre-commit.yaml?logo=pre-commit&logoColor=white&label=pre-commit&color=brightgreen)](https://github.com/isaac-sim/IsaacLab/actions/workflows/pre-commit.yaml) @@ -27,6 +27,13 @@ In addition to what IsaacLab provides, UW Lab brings: - **Sim to Real**: Providing robots and configuration that has been tested in Lab and deliver the Simulation Setup that can directly transfer to reals +## Release line + +UWLab 2.0 targets Isaac Lab **3.0 Early Access**, Isaac Sim **6.1**, Python **3.12**, and the released UW-Lab RSL-RL **5.4.1** integration. +The installer pins Isaac Lab to `ae37b028ea415c91ea2bc32609efcd759ed2b974` and RSL-RL to `2c3bf18001a5e2a78527e9ea368b7ea31700a2c5` (`uw-v5.4.1`). +Use `isaaclab2` for the legacy Isaac Lab 2.x / Isaac Sim 5.1 stack. Do not mix its environments, datasets, or checkpoint layouts with this release. Use the already-compatible pretrained checkpoints linked in the OmniReset quick start. +The installer refuses to replace an Isaac Lab checkout with local changes. + ## Installation Follow the [installation guide](https://uw-lab.github.io/UWLab/main/source/setup/installation/index.html). diff --git a/VERSION b/VERSION index 3eefcb9d..227cea21 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +2.0.0 diff --git a/docker/.env.base b/docker/.env.base index 7be5c03f..91c91480 100644 --- a/docker/.env.base +++ b/docker/.env.base @@ -6,8 +6,8 @@ ACCEPT_EULA=Y # NVIDIA Isaac Sim base image ISAACSIM_BASE_IMAGE=nvcr.io/nvidia/isaac-sim -# NVIDIA Isaac Sim version to use (e.g. 5.1.0) -ISAACSIM_VERSION=5.1.0 +# NVIDIA Isaac Sim version to use (e.g. 6.1.0) +ISAACSIM_VERSION=6.1.0 # Derived from the default path in the NVIDIA provided Isaac Sim container DOCKER_ISAACSIM_ROOT_PATH=/isaac-sim # The UW Lab path in the container diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base index aca9ec5e..3f62de48 100644 --- a/docker/Dockerfile.base +++ b/docker/Dockerfile.base @@ -13,7 +13,7 @@ ENV ISAACSIM_VERSION=${ISAACSIM_VERSION_ARG} SHELL ["/bin/bash", "-c"] # Adds labels to the Dockerfile -LABEL version="2.1.1" +LABEL version="2.0.0" LABEL description="Dockerfile for building and running the UW Lab framework inside Isaac Sim container image." # Arguments diff --git a/docs/conf.py b/docs/conf.py index a454f895..a459bcbc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -127,9 +127,9 @@ "numpy": ("https://numpy.org/doc/stable/", None), "trimesh": ("https://trimesh.org/", None), "torch": ("https://pytorch.org/docs/stable/", None), - "isaacsim": ("https://docs.isaacsim.omniverse.nvidia.com/5.1.0/py/", None), + "isaacsim": ("https://docs.isaacsim.omniverse.nvidia.com/6.1.0/py/", None), "gymnasium": ("https://gymnasium.farama.org/", None), - "warp": ("https://nvidia.github.io/warp/", None), + "warp": ("https://nvidia.github.io/warp/stable/", None), "dev-guide": ("https://docs.omniverse.nvidia.com/dev-guide/latest", None), } @@ -264,7 +264,7 @@ { "name": "Isaac Sim", "url": "https://developer.nvidia.com/isaac-sim", - "icon": "https://img.shields.io/badge/IsaacSim-5.1.0-silver.svg", + "icon": "https://img.shields.io/badge/IsaacSim-6.1.0-silver.svg", "type": "url", }, { @@ -284,7 +284,7 @@ # 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|release/.*)$") +smv_branch_whitelist = os.getenv("SMV_BRANCH_WHITELIST", r"^(main|isaaclab2|devel|release/.*)$") # 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 = { diff --git a/docs/index.rst b/docs/index.rst index 222a3971..0e984bae 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,7 +28,7 @@ deeply with our vision. LICENSE -======= +======== The UW Lab framework is open-sourced under the BSD-3-Clause license. Please refer to :ref:`license` for more details. diff --git a/docs/source/_static/publications/omnireset/cube_success_rate_seeds.jpg b/docs/source/_static/publications/omnireset/cube_success_rate_seeds.jpg index 372346cd..2c0d1f69 100644 Binary files a/docs/source/_static/publications/omnireset/cube_success_rate_seeds.jpg and b/docs/source/_static/publications/omnireset/cube_success_rate_seeds.jpg differ diff --git a/docs/source/_static/publications/omnireset/cube_success_rate_seeds_walltime.jpg b/docs/source/_static/publications/omnireset/cube_success_rate_seeds_walltime.jpg index a225c660..c175abc8 100644 Binary files a/docs/source/_static/publications/omnireset/cube_success_rate_seeds_walltime.jpg and b/docs/source/_static/publications/omnireset/cube_success_rate_seeds_walltime.jpg differ diff --git a/docs/source/_static/publications/omnireset/cupcake_success_rate_seeds.jpg b/docs/source/_static/publications/omnireset/cupcake_success_rate_seeds.jpg index 92504c9c..3c8e6605 100644 Binary files a/docs/source/_static/publications/omnireset/cupcake_success_rate_seeds.jpg and b/docs/source/_static/publications/omnireset/cupcake_success_rate_seeds.jpg differ diff --git a/docs/source/_static/publications/omnireset/cupcake_success_rate_seeds_walltime.jpg b/docs/source/_static/publications/omnireset/cupcake_success_rate_seeds_walltime.jpg index d2399185..db9405f4 100644 Binary files a/docs/source/_static/publications/omnireset/cupcake_success_rate_seeds_walltime.jpg and b/docs/source/_static/publications/omnireset/cupcake_success_rate_seeds_walltime.jpg differ diff --git a/docs/source/_static/publications/omnireset/drawer_success_rate_seeds.jpg b/docs/source/_static/publications/omnireset/drawer_success_rate_seeds.jpg index 6eba60d5..5d3df42a 100644 Binary files a/docs/source/_static/publications/omnireset/drawer_success_rate_seeds.jpg and b/docs/source/_static/publications/omnireset/drawer_success_rate_seeds.jpg differ diff --git a/docs/source/_static/publications/omnireset/drawer_success_rate_seeds_walltime.jpg b/docs/source/_static/publications/omnireset/drawer_success_rate_seeds_walltime.jpg index 3c3970fe..7f798a9f 100644 Binary files a/docs/source/_static/publications/omnireset/drawer_success_rate_seeds_walltime.jpg and b/docs/source/_static/publications/omnireset/drawer_success_rate_seeds_walltime.jpg differ diff --git a/docs/source/_static/publications/omnireset/leg_success_rate_seeds.jpg b/docs/source/_static/publications/omnireset/leg_success_rate_seeds.jpg index f7a982bb..6dd368f7 100644 Binary files a/docs/source/_static/publications/omnireset/leg_success_rate_seeds.jpg and b/docs/source/_static/publications/omnireset/leg_success_rate_seeds.jpg differ diff --git a/docs/source/_static/publications/omnireset/leg_success_rate_seeds_walltime.jpg b/docs/source/_static/publications/omnireset/leg_success_rate_seeds_walltime.jpg index c2adbf3e..be2197eb 100644 Binary files a/docs/source/_static/publications/omnireset/leg_success_rate_seeds_walltime.jpg and b/docs/source/_static/publications/omnireset/leg_success_rate_seeds_walltime.jpg differ diff --git a/docs/source/_static/publications/omnireset/peg_success_rate_seeds.jpg b/docs/source/_static/publications/omnireset/peg_success_rate_seeds.jpg index d7f321ac..30ae87ac 100644 Binary files a/docs/source/_static/publications/omnireset/peg_success_rate_seeds.jpg and b/docs/source/_static/publications/omnireset/peg_success_rate_seeds.jpg differ diff --git a/docs/source/_static/publications/omnireset/peg_success_rate_seeds_walltime.jpg b/docs/source/_static/publications/omnireset/peg_success_rate_seeds_walltime.jpg index 83a20a47..fed1469f 100644 Binary files a/docs/source/_static/publications/omnireset/peg_success_rate_seeds_walltime.jpg and b/docs/source/_static/publications/omnireset/peg_success_rate_seeds_walltime.jpg differ diff --git a/docs/source/_static/publications/omnireset/rectangle_success_rate_seeds.jpg b/docs/source/_static/publications/omnireset/rectangle_success_rate_seeds.jpg index ff2d237a..78eadfde 100644 Binary files a/docs/source/_static/publications/omnireset/rectangle_success_rate_seeds.jpg and b/docs/source/_static/publications/omnireset/rectangle_success_rate_seeds.jpg differ diff --git a/docs/source/_static/publications/omnireset/rectangle_success_rate_seeds_walltime.jpg b/docs/source/_static/publications/omnireset/rectangle_success_rate_seeds_walltime.jpg index a7c7c0e1..d94d0fb0 100644 Binary files a/docs/source/_static/publications/omnireset/rectangle_success_rate_seeds_walltime.jpg and b/docs/source/_static/publications/omnireset/rectangle_success_rate_seeds_walltime.jpg differ diff --git a/docs/source/deployment/docker.rst b/docs/source/deployment/docker.rst index 832df708..8a41234e 100644 --- a/docs/source/deployment/docker.rst +++ b/docs/source/deployment/docker.rst @@ -363,6 +363,6 @@ To run an example within the container, run: .. _`several streaming clients`: https://docs.isaacsim.omniverse.nvidia.com/latest/installation/manual_livestream_clients.html .. _`known issue`: https://forums.developer.nvidia.com/t/unable-to-use-webrtc-when-i-run-runheadless-webrtc-sh-in-remote-headless-container/222916 .. _`profile`: https://docs.docker.com/compose/compose-file/15-profiles/ -.. _`apt package`: https://docs.ros.org/en/humble/Installation/Ubuntu-Install-Debians.html#install-ros-2-packages +.. _`apt package`: https://docs.ros.org/en/humble/Installation/Ubuntu-Install-Debs.html#install-ros-2-packages .. _`various middleware`: https://docs.ros.org/en/humble/How-To-Guides/Working-with-multiple-RMW-implementations.html .. _`tuned`: https://docs.ros.org/en/foxy/How-To-Guides/DDS-tuning.html diff --git a/docs/source/overview/developer-guide/development.rst b/docs/source/overview/developer-guide/development.rst index 4d586d20..d65a70ae 100644 --- a/docs/source/overview/developer-guide/development.rst +++ b/docs/source/overview/developer-guide/development.rst @@ -68,12 +68,12 @@ python package to build the python module provided by the extensions. This is do .. note:: The ``setup.py`` file is not required for extensions that are only loaded into Omniverse - using the `Extension Manager `__. + using the `Extension Manager `__. Lastly, the ``tests`` directory contains the unit tests for the extension. These are written using the `unittest `__ framework. It is important to note that Omniverse also provides a similar -`testing framework `__. +`testing framework `__. However, it requires going through the build process and does not support testing of the python module in standalone applications. diff --git a/docs/source/overview/isaac_environments.rst b/docs/source/overview/isaac_environments.rst index e601fa2f..3b798d2d 100644 --- a/docs/source/overview/isaac_environments.rst +++ b/docs/source/overview/isaac_environments.rst @@ -1,1137 +1,41 @@ .. _environments: -Available Environments +Isaac Lab Environments ====================== -The following lists comprises of all the RL and IL tasks implementations that are available in UW Lab. -While we try to keep this list up-to-date, you can always get the latest list of environments by -running the following command: +Upstream Isaac Lab environments are supplied by the pinned Isaac Lab dependency, +not by ``source/uwlab_tasks``. UWLab 2.0 uses Isaac Lab 3.0 Early Access at +``ae37b028ea415c91ea2bc32609efcd759ed2b974``. Task names, configuration locations, +and backend presets differ from the older 2.x catalogue. -.. tab-set:: - :sync-group: os +Use the `maintained Isaac Lab environment browser +`_ +to explore upstream tasks. That live page follows upstream development; the +`task registrations at UWLab's pinned revision +`_ +and the `browser source at that revision +`_ +are the version-specific references for this release. - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux +Discover installed environments +------------------------------- - .. code:: bash +After installing the pinned stack, list the environments registered by the local +checkout rather than assuming that a name from another release is available: - ./uwlab.sh -p scripts/environments/list_envs.py +.. code-block:: bash - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows + ./uwlab.sh -p scripts/environments/list_envs.py - .. code:: batch +For UWLab's own tasks and configurations, see :doc:`uw_environments`. +Availability in the registry does not by itself establish runtime qualification +of every task, robot, or backend combination. - uwlab.bat -p scripts\environments\list_envs.py +Legacy environments +------------------- -We are actively working on adding more environments to the list. If you have any environments that -you would like to add to UW Lab, please feel free to open a pull request! - -Single-agent ------------- - -Classic -~~~~~~~ - -Classic environments that are based on IsaacGymEnvs implementation of MuJoCo-style environments. - -.. table:: - :widths: 33 37 30 - - +------------------+-----------------------------+-------------------------------------------------------------------------+ - | World | Environment ID | Description | - +==================+=============================+=========================================================================+ - | |humanoid| | |humanoid-link| | Move towards a direction with the MuJoCo humanoid robot | - | | | | - | | |humanoid-direct-link| | | - +------------------+-----------------------------+-------------------------------------------------------------------------+ - | |ant| | |ant-link| | Move towards a direction with the MuJoCo ant robot | - | | | | - | | |ant-direct-link| | | - +------------------+-----------------------------+-------------------------------------------------------------------------+ - | |cartpole| | |cartpole-link| | Move the cart to keep the pole upwards in the classic cartpole control | - | | | | - | | |cartpole-direct-link| | | - +------------------+-----------------------------+-------------------------------------------------------------------------+ - | |cartpole| | |cartpole-rgb-link| | Move the cart to keep the pole upwards in the classic cartpole control | - | | | and perceptive inputs. Requires running with ``--enable_cameras``. | - | | |cartpole-depth-link| | | - | | | | - | | |cartpole-rgb-direct-link| | | - | | | | - | | |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. Requires running with ``--enable_cameras``. | - +------------------+-----------------------------+-------------------------------------------------------------------------+ - -.. |humanoid| image:: ../_static/tasks/classic/humanoid.jpg -.. |ant| image:: ../_static/tasks/classic/ant.jpg -.. |cartpole| image:: ../_static/tasks/classic/cartpole.jpg - -.. |humanoid-link| replace:: `Isaac-Humanoid-v0 `__ -.. |ant-link| replace:: `Isaac-Ant-v0 `__ -.. |cartpole-link| replace:: `Isaac-Cartpole-v0 `__ -.. |cartpole-rgb-link| replace:: `Isaac-Cartpole-RGB-v0 `__ -.. |cartpole-depth-link| replace:: `Isaac-Cartpole-Depth-v0 `__ -.. |cartpole-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 `__ -.. |cartpole-direct-link| replace:: `Isaac-Cartpole-Direct-v0 `__ -.. |cartpole-rgb-direct-link| replace:: `Isaac-Cartpole-RGB-Camera-Direct-v0 `__ -.. |cartpole-depth-direct-link| replace:: `Isaac-Cartpole-Depth-Camera-Direct-v0 `__ - -Manipulation -~~~~~~~~~~~~ - -Environments based on fixed-arm manipulation tasks. - -For many of these tasks, we include configurations with different arm action spaces. For example, -for the lift-cube environment: - -* |lift-cube-link|: Franka arm with joint position control -* |lift-cube-ik-abs-link|: Franka arm with absolute IK control -* |lift-cube-ik-rel-link|: Franka arm with relative IK control - -.. table:: - :widths: 33 37 30 - - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | World | Environment ID | Description | - +=========================+==============================+=============================================================================+ - | |reach-franka| | |reach-franka-link| | Move the end-effector to a sampled target pose with the Franka robot | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |reach-ur10| | |reach-ur10-link| | Move the end-effector to a sampled target pose with the UR10 robot | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |deploy-reach-ur10e| | |deploy-reach-ur10e-link| | Move the end-effector to a sampled target pose with the UR10e robot | - | | | This policy has been deployed to a real robot | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |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. | - | | | Blueprint env used for the NVIDIA Isaac GR00T blueprint for synthetic | - | | |stack-cube-bp-link| | manipulation motion generation | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |surface-gripper| | |long-suction-link| | Stack three cubes (bottom to top: blue, red, green) | - | | | with the UR10 arm and long surface gripper | - | | |short-suction-link| | or short surface gripper. | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |cabi-franka| | |cabi-franka-link| | Grasp the handle of a cabinet's drawer and open it with the Franka robot | - | | | | - | | |franka-direct-link| | | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |cube-allegro| | |cube-allegro-link| | In-hand reorientation of a cube using Allegro hand | - | | | | - | | |allegro-direct-link| | | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |cube-shadow| | |cube-shadow-link| | In-hand reorientation of a cube using Shadow hand | - | | | | - | | |cube-shadow-ff-link| | | - | | | | - | | |cube-shadow-lstm-link| | | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |cube-shadow| | |cube-shadow-vis-link| | In-hand reorientation of a cube using Shadow hand using perceptive inputs. | - | | | Requires running with ``--enable_cameras``. | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |gr1_pick_place| | |gr1_pick_place-link| | Pick up and place an object in a basket with a GR-1 humanoid robot | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |gr1_pp_waist| | |gr1_pp_waist-link| | Pick up and place an object in a basket with a GR-1 humanoid robot | - | | | with waist degrees-of-freedom enables that provides a wider reach space. | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |g1_pick_place| | |g1_pick_place-link| | Pick up and place an object in a basket with a Unitree G1 humanoid robot | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |g1_pick_place_fixed| | |g1_pick_place_fixed-link| | Pick up and place an object in a basket with a Unitree G1 humanoid robot | - | | | with three-fingered hands. Robot is set up with the base fixed in place. | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |g1_pick_place_lm| | |g1_pick_place_lm-link| | Pick up and place an object in a basket with a Unitree G1 humanoid robot | - | | | with three-fingered hands and in-place locomanipulation capabilities | - | | | enabled (i.e. Robot lower body balances in-place while upper body is | - | | | controlled via Inverse Kinematics). | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |kuka-allegro-lift| | |kuka-allegro-lift-link| | Pick up a primitive shape on the table and lift it to target position | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |kuka-allegro-reorient| | |kuka-allegro-reorient-link| | Pick up a primitive shape on the table and orient it to target pose | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |galbot_stack| | |galbot_stack-link| | Stack three cubes (bottom to top: blue, red, green) with the left arm of | - | | | a Galbot humanoid robot | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |agibot_place_mug| | |agibot_place_mug-link| | Pick up and place a mug upright with a Agibot A2D humanoid robot | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - | |agibot_place_toy| | |agibot_place_toy-link| | Pick up and place an object in a box with a Agibot A2D humanoid robot | - +-------------------------+------------------------------+-----------------------------------------------------------------------------+ - -.. |reach-franka| image:: ../_static/tasks/manipulation/franka_reach.jpg -.. |reach-ur10| image:: ../_static/tasks/manipulation/ur10_reach.jpg -.. |deploy-reach-ur10e| image:: ../_static/tasks/manipulation/ur10e_reach.jpg -.. |lift-cube| image:: ../_static/tasks/manipulation/franka_lift.jpg -.. |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-cube| image:: ../_static/tasks/manipulation/franka_stack.jpg -.. |gr1_pick_place| image:: ../_static/tasks/manipulation/gr-1_pick_place.jpg -.. |g1_pick_place| image:: ../_static/tasks/manipulation/g1_pick_place.jpg -.. |g1_pick_place_fixed| image:: ../_static/tasks/manipulation/g1_pick_place_fixed_base.jpg -.. |g1_pick_place_lm| image:: ../_static/tasks/manipulation/g1_pick_place_locomanipulation.jpg -.. |surface-gripper| image:: ../_static/tasks/manipulation/ur10_stack_surface_gripper.jpg -.. |gr1_pp_waist| image:: ../_static/tasks/manipulation/gr-1_pick_place_waist.jpg -.. |galbot_stack| image:: ../_static/tasks/manipulation/galbot_stack_cube.jpg -.. |agibot_place_mug| image:: ../_static/tasks/manipulation/agibot_place_mug.jpg -.. |agibot_place_toy| image:: ../_static/tasks/manipulation/agibot_place_toy.jpg -.. |kuka-allegro-lift| image:: ../_static/tasks/manipulation/kuka_allegro_lift.jpg -.. |kuka-allegro-reorient| image:: ../_static/tasks/manipulation/kuka_allegro_reorient.jpg - -.. |reach-franka-link| replace:: `Isaac-Reach-Franka-v0 `__ -.. |reach-ur10-link| replace:: `Isaac-Reach-UR10-v0 `__ -.. |deploy-reach-ur10e-link| replace:: `Isaac-Deploy-Reach-UR10e-v0 `__ -.. |lift-cube-link| replace:: `Isaac-Lift-Cube-Franka-v0 `__ -.. |lift-cube-ik-abs-link| replace:: `Isaac-Lift-Cube-Franka-IK-Abs-v0 `__ -.. |lift-cube-ik-rel-link| replace:: `Isaac-Lift-Cube-Franka-IK-Rel-v0 `__ -.. |cabi-franka-link| replace:: `Isaac-Open-Drawer-Franka-v0 `__ -.. |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 `__ -.. |stack-cube-bp-link| replace:: `Isaac-Stack-Cube-Franka-IK-Rel-Blueprint-v0 `__ -.. |gr1_pick_place-link| replace:: `Isaac-PickPlace-GR1T2-Abs-v0 `__ -.. |g1_pick_place-link| replace:: `Isaac-PickPlace-G1-InspireFTP-Abs-v0 `__ -.. |g1_pick_place_fixed-link| replace:: `Isaac-PickPlace-FixedBaseUpperBodyIK-G1-Abs-v0 `__ -.. |g1_pick_place_lm-link| replace:: `Isaac-PickPlace-Locomanipulation-G1-Abs-v0 `__ -.. |long-suction-link| replace:: `Isaac-Stack-Cube-UR10-Long-Suction-IK-Rel-v0 `__ -.. |short-suction-link| replace:: `Isaac-Stack-Cube-UR10-Short-Suction-IK-Rel-v0 `__ -.. |gr1_pp_waist-link| replace:: `Isaac-PickPlace-GR1T2-WaistEnabled-Abs-v0 `__ -.. |galbot_stack-link| replace:: `Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-v0 `__ -.. |kuka-allegro-lift-link| replace:: `Isaac-Dexsuite-Kuka-Allegro-Lift-v0 `__ -.. |kuka-allegro-reorient-link| replace:: `Isaac-Dexsuite-Kuka-Allegro-Reorient-v0 `__ -.. |cube-shadow-link| replace:: `Isaac-Repose-Cube-Shadow-Direct-v0 `__ -.. |cube-shadow-ff-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-FF-Direct-v0 `__ -.. |cube-shadow-lstm-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-LSTM-Direct-v0 `__ -.. |cube-shadow-vis-link| replace:: `Isaac-Repose-Cube-Shadow-Vision-Direct-v0 `__ -.. |agibot_place_mug-link| replace:: `Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-v0 `__ -.. |agibot_place_toy-link| replace:: `Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 `__ - - -Contact-rich Manipulation -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Environments based on contact-rich manipulation tasks such as peg insertion, gear meshing and nut-bolt fastening. - -These tasks share the same task configurations and control options. You can switch between them by specifying the task name. -For example: - -* |factory-peg-link|: Peg insertion with the Franka arm -* |factory-gear-link|: Gear meshing with the Franka arm -* |factory-nut-link|: Nut-Bolt fastening with the Franka arm - -.. table:: - :widths: 33 37 30 - - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | World | Environment ID | Description | - +====================+=========================+=============================================================================+ - | |factory-peg| | |factory-peg-link| | Insert peg into the socket with the Franka robot | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | |factory-gear| | |factory-gear-link| | Insert and mesh gear into the base with other gears, using the Franka robot | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | |factory-nut| | |factory-nut-link| | Thread the nut onto the first 2 threads of the bolt, using the Franka robot | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - -.. |factory-peg| image:: ../_static/tasks/factory/peg_insert.jpg -.. |factory-gear| image:: ../_static/tasks/factory/gear_mesh.jpg -.. |factory-nut| image:: ../_static/tasks/factory/nut_thread.jpg - -.. |factory-peg-link| replace:: `Isaac-Factory-PegInsert-Direct-v0 `__ -.. |factory-gear-link| replace:: `Isaac-Factory-GearMesh-Direct-v0 `__ -.. |factory-nut-link| replace:: `Isaac-Factory-NutThread-Direct-v0 `__ - -AutoMate -~~~~~~~~ - -Environments based on 100 diverse assembly tasks, each involving the insertion of a plug into a socket. These tasks share a common configuration and differ by th geometry and properties of the parts. - -You can switch between tasks by specifying the corresponding asset ID. Available asset IDs include: - -'00004', '00007', '00014', '00015', '00016', '00021', '00028', '00030', '00032', '00042', '00062', '00074', '00077', '00078', '00081', '00083', '00103', '00110', '00117', '00133', '00138', '00141', '00143', '00163', '00175', '00186', '00187', '00190', '00192', '00210', '00211', '00213', '00255', '00256', '00271', '00293', '00296', '00301', '00308', '00318', '00319', '00320', '00329', '00340', '00345', '00346', '00360', '00388', '00410', '00417', '00422', '00426', '00437', '00444', '00446', '00470', '00471', '00480', '00486', '00499', '00506', '00514', '00537', '00553', '00559', '00581', '00597', '00614', '00615', '00638', '00648', '00649', '00652', '00659', '00681', '00686', '00700', '00703', '00726', '00731', '00741', '00755', '00768', '00783', '00831', '00855', '00860', '00863', '01026', '01029', '01036', '01041', '01053', '01079', '01092', '01102', '01125', '01129', '01132', '01136'. - -We provide environments for both disassembly and assembly. - -.. attention:: - - CUDA is recommended for running the AutoMate environments with 570 drivers. If running with Nvidia driver 570 on Linux with architecture x86_64, we follow the below steps to install CUDA 12.8. This allows for computing rewards in AutoMate environments with CUDA. If you have a different operation system or architecture, please refer to the `CUDA installation page `_ for additional instruction. - - .. code-block:: bash - - wget https://developer.download.nvidia.com/compute/cuda/12.8.0/local_installers/cuda_12.8.0_570.86.10_linux.run - sudo sh cuda_12.8.0_570.86.10_linux.run --toolkit - - When using conda, cuda toolkit can be installed with: - - .. code-block:: bash - - conda install cudatoolkit - - With 580 drivers and CUDA 13, we are currently unable to enable CUDA for computing the rewards. The code automatically fallbacks to CPU, resulting in slightly slower performance. - -* |disassembly-link|: The plug starts inserted in the socket. A low-level controller lifts the plug out and moves it to a random position. This process is purely scripted and does not involve any learned policy. Therefore, it does not require policy training or evaluation. The resulting trajectories serve as demonstrations for the reverse process, i.e., learning to assemble. To run disassembly for a specific task: ``python source/uwlab_tasks/uwlab_tasks/direct/automate/run_disassembly_w_id.py --assembly_id=ASSEMBLY_ID --disassembly_dir=DISASSEMBLY_DIR``. All generated trajectories are saved to a local directory ``DISASSEMBLY_DIR``. -* |assembly-link|: The goal is to insert the plug into the socket. You can use this environment to train a policy via reinforcement learning or evaluate a pre-trained checkpoint. - - * To train an assembly policy, we run the command ``python source/uwlab_tasks/uwlab_tasks/direct/automate/run_w_id.py --assembly_id=ASSEMBLY_ID --train``. We can customize the training process using the optional flags: ``--headless`` to run without opening the GUI windows, ``--max_iterations=MAX_ITERATIONS`` to set the number of training iterations, ``--num_envs=NUM_ENVS`` to set the number of parallel environments during training, ``--seed=SEED`` to assign the random seed. The policy checkpoints will be saved automatically during training in the directory ``logs/rl_games/Assembly/test``. - * To evaluate an assembly policy, we run the command ``python source/uwlab_tasks/uwlab_tasks/direct/automate/run_w_id.py --assembly_id=ASSEMBLY_ID --checkpoint=CHECKPOINT --log_eval``. The evaluation results are stored in ``evaluation_{ASSEMBLY_ID}.h5``. - -.. table:: - :widths: 33 37 30 - - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | World | Environment ID | Description | - +====================+=========================+=============================================================================+ - | |disassembly| | |disassembly-link| | Lift a plug out of the socket with the Franka robot | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | |assembly| | |assembly-link| | Insert a plug into its corresponding socket with the Franka robot | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - -.. |assembly| image:: ../_static/tasks/automate/00004.jpg -.. |disassembly| image:: ../_static/tasks/automate/01053_disassembly.jpg - -.. |assembly-link| replace:: `Isaac-AutoMate-Assembly-Direct-v0 `__ -.. |disassembly-link| replace:: `Isaac-AutoMate-Disassembly-Direct-v0 `__ - -FORGE -~~~~~~~~ - -FORGE environments extend Factory environments with: - -* Force sensing: Add observations for force experienced by the end-effector. -* Excessive force penalty: Add an option to penalize the agent for excessive contact forces. -* Dynamics randomization: Randomize controller gains, asset properties (friction, mass), and dead-zone. -* Success prediction: Add an extra action that predicts task success. - -These tasks share the same task configurations and control options. You can switch between them by specifying the task name. - -* |forge-peg-link|: Peg insertion with the Franka arm -* |forge-gear-link|: Gear meshing with the Franka arm -* |forge-nut-link|: Nut-Bolt fastening with the Franka arm - -.. table:: - :widths: 33 37 30 - - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | World | Environment ID | Description | - +====================+=========================+=============================================================================+ - | |forge-peg| | |forge-peg-link| | Insert peg into the socket with the Franka robot | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | |forge-gear| | |forge-gear-link| | Insert and mesh gear into the base with other gears, using the Franka robot | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | |forge-nut| | |forge-nut-link| | Thread the nut onto the first 2 threads of the bolt, using the Franka robot | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - -.. |forge-peg| image:: ../_static/tasks/factory/peg_insert.jpg -.. |forge-gear| image:: ../_static/tasks/factory/gear_mesh.jpg -.. |forge-nut| image:: ../_static/tasks/factory/nut_thread.jpg - -.. |forge-peg-link| replace:: `Isaac-Forge-PegInsert-Direct-v0 `__ -.. |forge-gear-link| replace:: `Isaac-Forge-GearMesh-Direct-v0 `__ -.. |forge-nut-link| replace:: `Isaac-Forge-NutThread-Direct-v0 `__ - - -Locomotion -~~~~~~~~~~ - -Environments based on legged locomotion tasks. - -.. table:: - :widths: 33 37 30 - - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | World | Environment ID | Description | - +==============================+==============================================+==============================================================================+ - | |velocity-flat-anymal-b| | |velocity-flat-anymal-b-link| | Track a velocity command on flat terrain with the Anymal B robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-rough-anymal-b| | |velocity-rough-anymal-b-link| | Track a velocity command on rough terrain with the Anymal B robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-flat-anymal-c| | |velocity-flat-anymal-c-link| | Track a velocity command on flat terrain with the Anymal C robot | - | | | | - | | |velocity-flat-anymal-c-direct-link| | | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-rough-anymal-c| | |velocity-rough-anymal-c-link| | Track a velocity command on rough terrain with the Anymal C robot | - | | | | - | | |velocity-rough-anymal-c-direct-link| | | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-flat-anymal-d| | |velocity-flat-anymal-d-link| | Track a velocity command on flat terrain with the Anymal D robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-rough-anymal-d| | |velocity-rough-anymal-d-link| | Track a velocity command on rough terrain with the Anymal D robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-flat-unitree-a1| | |velocity-flat-unitree-a1-link| | Track a velocity command on flat terrain with the Unitree A1 robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-rough-unitree-a1| | |velocity-rough-unitree-a1-link| | Track a velocity command on rough terrain with the Unitree A1 robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-flat-unitree-go1| | |velocity-flat-unitree-go1-link| | Track a velocity command on flat terrain with the Unitree Go1 robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-rough-unitree-go1| | |velocity-rough-unitree-go1-link| | Track a velocity command on rough terrain with the Unitree Go1 robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-flat-unitree-go2| | |velocity-flat-unitree-go2-link| | Track a velocity command on flat terrain with the Unitree Go2 robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-rough-unitree-go2| | |velocity-rough-unitree-go2-link| | Track a velocity command on rough terrain with the Unitree Go2 robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-flat-spot| | |velocity-flat-spot-link| | Track a velocity command on flat terrain with the Boston Dynamics Spot robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-flat-h1| | |velocity-flat-h1-link| | Track a velocity command on flat terrain with the Unitree H1 robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-rough-h1| | |velocity-rough-h1-link| | Track a velocity command on rough terrain with the Unitree H1 robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-flat-g1| | |velocity-flat-g1-link| | Track a velocity command on flat terrain with the Unitree G1 robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-rough-g1| | |velocity-rough-g1-link| | Track a velocity command on rough terrain with the Unitree G1 robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-flat-digit| | |velocity-flat-digit-link| | Track a velocity command on flat terrain with the Agility Digit robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |velocity-rough-digit| | |velocity-rough-digit-link| | Track a velocity command on rough terrain with the Agility Digit robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - | |tracking-loco-manip-digit| | |tracking-loco-manip-digit-link| | Track a root velocity and hand pose command with the Agility Digit robot | - +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ - -.. |velocity-flat-anymal-b-link| replace:: `Isaac-Velocity-Flat-Anymal-B-v0 `__ -.. |velocity-rough-anymal-b-link| replace:: `Isaac-Velocity-Rough-Anymal-B-v0 `__ - -.. |velocity-flat-anymal-c-link| replace:: `Isaac-Velocity-Flat-Anymal-C-v0 `__ -.. |velocity-rough-anymal-c-link| replace:: `Isaac-Velocity-Rough-Anymal-C-v0 `__ - -.. |velocity-flat-anymal-c-direct-link| replace:: `Isaac-Velocity-Flat-Anymal-C-Direct-v0 `__ -.. |velocity-rough-anymal-c-direct-link| replace:: `Isaac-Velocity-Rough-Anymal-C-Direct-v0 `__ - -.. |velocity-flat-anymal-d-link| replace:: `Isaac-Velocity-Flat-Anymal-D-v0 `__ -.. |velocity-rough-anymal-d-link| replace:: `Isaac-Velocity-Rough-Anymal-D-v0 `__ - -.. |velocity-flat-unitree-a1-link| replace:: `Isaac-Velocity-Flat-Unitree-A1-v0 `__ -.. |velocity-rough-unitree-a1-link| replace:: `Isaac-Velocity-Rough-Unitree-A1-v0 `__ - -.. |velocity-flat-unitree-go1-link| replace:: `Isaac-Velocity-Flat-Unitree-Go1-v0 `__ -.. |velocity-rough-unitree-go1-link| replace:: `Isaac-Velocity-Rough-Unitree-Go1-v0 `__ - -.. |velocity-flat-unitree-go2-link| replace:: `Isaac-Velocity-Flat-Unitree-Go2-v0 `__ -.. |velocity-rough-unitree-go2-link| replace:: `Isaac-Velocity-Rough-Unitree-Go2-v0 `__ - -.. |velocity-flat-spot-link| replace:: `Isaac-Velocity-Flat-Spot-v0 `__ - -.. |velocity-flat-h1-link| replace:: `Isaac-Velocity-Flat-H1-v0 `__ -.. |velocity-rough-h1-link| replace:: `Isaac-Velocity-Rough-H1-v0 `__ - -.. |velocity-flat-g1-link| replace:: `Isaac-Velocity-Flat-G1-v0 `__ -.. |velocity-rough-g1-link| replace:: `Isaac-Velocity-Rough-G1-v0 `__ - -.. |velocity-flat-digit-link| replace:: `Isaac-Velocity-Flat-Digit-v0 `__ -.. |velocity-rough-digit-link| replace:: `Isaac-Velocity-Rough-Digit-v0 `__ -.. |tracking-loco-manip-digit-link| replace:: `Isaac-Tracking-LocoManip-Digit-v0 `__ - -.. |velocity-flat-anymal-b| image:: ../_static/tasks/locomotion/anymal_b_flat.jpg -.. |velocity-rough-anymal-b| image:: ../_static/tasks/locomotion/anymal_b_rough.jpg -.. |velocity-flat-anymal-c| image:: ../_static/tasks/locomotion/anymal_c_flat.jpg -.. |velocity-rough-anymal-c| image:: ../_static/tasks/locomotion/anymal_c_rough.jpg -.. |velocity-flat-anymal-d| image:: ../_static/tasks/locomotion/anymal_d_flat.jpg -.. |velocity-rough-anymal-d| image:: ../_static/tasks/locomotion/anymal_d_rough.jpg -.. |velocity-flat-unitree-a1| image:: ../_static/tasks/locomotion/a1_flat.jpg -.. |velocity-rough-unitree-a1| image:: ../_static/tasks/locomotion/a1_rough.jpg -.. |velocity-flat-unitree-go1| image:: ../_static/tasks/locomotion/go1_flat.jpg -.. |velocity-rough-unitree-go1| image:: ../_static/tasks/locomotion/go1_rough.jpg -.. |velocity-flat-unitree-go2| image:: ../_static/tasks/locomotion/go2_flat.jpg -.. |velocity-rough-unitree-go2| image:: ../_static/tasks/locomotion/go2_rough.jpg -.. |velocity-flat-spot| image:: ../_static/tasks/locomotion/spot_flat.jpg -.. |velocity-flat-h1| image:: ../_static/tasks/locomotion/h1_flat.jpg -.. |velocity-rough-h1| image:: ../_static/tasks/locomotion/h1_rough.jpg -.. |velocity-flat-g1| image:: ../_static/tasks/locomotion/g1_flat.jpg -.. |velocity-rough-g1| image:: ../_static/tasks/locomotion/g1_rough.jpg -.. |velocity-flat-digit| image:: ../_static/tasks/locomotion/agility_digit_flat.jpg -.. |velocity-rough-digit| image:: ../_static/tasks/locomotion/agility_digit_rough.jpg -.. |tracking-loco-manip-digit| image:: ../_static/tasks/locomotion/agility_digit_loco_manip.jpg - -Navigation -~~~~~~~~~~ - -.. table:: - :widths: 33 37 30 - - +----------------+---------------------+-----------------------------------------------------------------------------+ - | World | Environment ID | Description | - +================+=====================+=============================================================================+ - | |anymal_c_nav| | |anymal_c_nav-link| | Navigate towards a target x-y position and heading with the ANYmal C robot. | - +----------------+---------------------+-----------------------------------------------------------------------------+ - -.. |anymal_c_nav-link| replace:: `Isaac-Navigation-Flat-Anymal-C-v0 `__ - -.. |anymal_c_nav| image:: ../_static/tasks/navigation/anymal_c_nav.jpg - - -Others -~~~~~~ - -.. note:: - - Adversarial Motion Priors (AMP) training is only available with the `skrl` library, as it is the only one of the currently - integrated libraries that supports it out-of-the-box (for the other libraries, it is necessary to implement the algorithm and architectures). - See the `skrl's AMP Documentation `_ for more information. - The AMP algorithm can be activated by adding the command line input ``--algorithm AMP`` to the train/play script. - - For evaluation, the play script's command line input ``--real-time`` allows the interaction loop between the environment and the agent to run in real time, if possible. - -.. table:: - :widths: 33 37 30 - - +----------------+---------------------------+-----------------------------------------------------------------------------+ - | World | Environment ID | Description | - +================+===========================+=============================================================================+ - | |quadcopter| | |quadcopter-link| | Fly and hover the Crazyflie copter at a goal point by applying thrust. | - +----------------+---------------------------+-----------------------------------------------------------------------------+ - | |humanoid_amp| | |humanoid_amp_dance-link| | Move a humanoid robot by imitating different pre-recorded human animations | - | | | (Adversarial Motion Priors). | - | | |humanoid_amp_run-link| | | - | | | | - | | |humanoid_amp_walk-link| | | - +----------------+---------------------------+-----------------------------------------------------------------------------+ - -.. |quadcopter-link| replace:: `Isaac-Quadcopter-Direct-v0 `__ -.. |humanoid_amp_dance-link| replace:: `Isaac-Humanoid-AMP-Dance-Direct-v0 `__ -.. |humanoid_amp_run-link| replace:: `Isaac-Humanoid-AMP-Run-Direct-v0 `__ -.. |humanoid_amp_walk-link| replace:: `Isaac-Humanoid-AMP-Walk-Direct-v0 `__ - -.. |quadcopter| image:: ../_static/tasks/others/quadcopter.jpg -.. |humanoid_amp| image:: ../_static/tasks/others/humanoid_amp.jpg - -Spaces showcase -~~~~~~~~~~~~~~~ - -The |cartpole_showcase| folder contains showcase tasks (based on the *Cartpole* and *Cartpole-Camera* Direct tasks) -for the definition/use of the various Gymnasium observation and action spaces supported in UW Lab. - -.. |cartpole_showcase| replace:: `cartpole_showcase `__ - -.. note:: - - Currently, only UW Lab's Direct workflow supports the definition of observation and action spaces other than ``Box``. - See Direct workflow's :py:obj:`~uwlab.envs.DirectRLEnvCfg.observation_space` / :py:obj:`~uwlab.envs.DirectRLEnvCfg.action_space` - documentation for more details. - -The following tables summarize the different pairs of showcased spaces for the *Cartpole* and *Cartpole-Camera* tasks. -Replace ```` and ```` with the observation and action spaces to be explored in the task names for training and evaluation. - -.. raw:: html - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Showcase spaces for the Cartpole task

-

Isaac-Cartpole-Showcase-<OBSERVATION>-<ACTION>-Direct-v0

-
action space
 Box Discrete MultiDiscrete

observation

space

 Boxxxx
 Discretexxx
 MultiDiscretexxx
 Dictxxx
 Tuplexxx
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Showcase spaces for the Cartpole-Camera task

-

Isaac-Cartpole-Camera-Showcase-<OBSERVATION>-<ACTION>-Direct-v0

-
action space
 Box Discrete MultiDiscrete

observation

space

 Boxxxx
 Discrete---
 MultiDiscrete---
 Dictxxx
 Tuplexxx
- -Multi-agent ------------- - -.. note:: - - True mutli-agent training is only available with the `skrl` library, see the `Multi-Agents Documentation `_ for more information. - It supports the `IPPO` and `MAPPO` algorithms, which can be activated by adding the command line input ``--algorithm IPPO`` or ``--algorithm MAPPO`` to the train/play script. - If these environments are run with other libraries or without the `IPPO` or `MAPPO` flags, they will be converted to single-agent environments under the hood. - - -Classic -~~~~~~~ - -.. table:: - :widths: 33 37 30 - - +------------------------+------------------------------------+-----------------------------------------------------------------------------------------------------------------------+ - | World | Environment ID | Description | - +========================+====================================+=======================================================================================================================+ - | |cart-double-pendulum| | |cart-double-pendulum-direct-link| | Move the cart and the pendulum to keep the last one upwards in the classic inverted double pendulum on a cart control | - +------------------------+------------------------------------+-----------------------------------------------------------------------------------------------------------------------+ - -.. |cart-double-pendulum| image:: ../_static/tasks/classic/cart_double_pendulum.jpg - -.. |cart-double-pendulum-direct-link| replace:: `Isaac-Cart-Double-Pendulum-Direct-v0 `__ - -Manipulation -~~~~~~~~~~~~ - -Environments based on fixed-arm manipulation tasks. - -.. table:: - :widths: 33 37 30 - - +----------------------+--------------------------------+--------------------------------------------------------+ - | World | Environment ID | Description | - +======================+================================+========================================================+ - | |shadow-hand-over| | |shadow-hand-over-direct-link| | Passing an object from one hand over to the other hand | - +----------------------+--------------------------------+--------------------------------------------------------+ - -.. |shadow-hand-over| image:: ../_static/tasks/manipulation/shadow_hand_over.jpg - -.. |shadow-hand-over-direct-link| replace:: `Isaac-Shadow-Hand-Over-Direct-v0 `__ - -| - -Comprehensive List of Environments -================================== - -For environments that have a different task name listed under ``Inference Task Name``, please use the Inference Task Name -provided when running ``play.py`` or any inferencing workflows. These tasks provide more suitable configurations for -inferencing, including reading from an already trained checkpoint and disabling runtime perturbations used for training. - -.. list-table:: - :widths: 33 25 19 25 - - * - **Task Name** - - **Inference Task Name** - - **Workflow** - - **RL Library** - * - Isaac-Ant-Direct-v0 - - - - Direct - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Ant-v0 - - - - Manager Based - - **rsl_rl** (PPO), **rl_games** (PPO), **skrl** (PPO), **sb3** (PPO) - * - Isaac-Cart-Double-Pendulum-Direct-v0 - - - - Direct - - **rl_games** (PPO), **skrl** (IPPO, PPO, MAPPO) - * - Isaac-Cartpole-Camera-Showcase-Box-Box-Direct-v0 (Requires running with ``--enable_cameras``) - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Camera-Showcase-Box-Discrete-Direct-v0 (Requires running with ``--enable_cameras``) - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Camera-Showcase-Box-MultiDiscrete-Direct-v0 (Requires running with ``--enable_cameras``) - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Camera-Showcase-Dict-Box-Direct-v0 (Requires running with ``--enable_cameras``) - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Camera-Showcase-Dict-Discrete-Direct-v0 (Requires running with ``--enable_cameras``) - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Camera-Showcase-Dict-MultiDiscrete-Direct-v0 (Requires running with ``--enable_cameras``) - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Camera-Showcase-Tuple-Box-Direct-v0 (Requires running with ``--enable_cameras``) - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Camera-Showcase-Tuple-Discrete-Direct-v0 (Requires running with ``--enable_cameras``) - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Camera-Showcase-Tuple-MultiDiscrete-Direct-v0 (Requires running with ``--enable_cameras``) - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Depth-Camera-Direct-v0 (Requires running with ``--enable_cameras``) - - - - Direct - - **rl_games** (PPO), **skrl** (PPO) - * - Isaac-Cartpole-Depth-v0 (Requires running with ``--enable_cameras``) - - - - Manager Based - - **rl_games** (PPO) - * - Isaac-Cartpole-Direct-v0 - - - - Direct - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO), **sb3** (PPO) - * - Isaac-Cartpole-RGB-Camera-Direct-v0 (Requires running with ``--enable_cameras``) - - - - Direct - - **rl_games** (PPO), **skrl** (PPO) - * - Isaac-Cartpole-RGB-ResNet18-v0 (Requires running with ``--enable_cameras``) - - - - Manager Based - - **rl_games** (PPO) - * - Isaac-Cartpole-RGB-TheiaTiny-v0 (Requires running with ``--enable_cameras``) - - - - Manager Based - - **rl_games** (PPO) - * - Isaac-Cartpole-RGB-v0 (Requires running with ``--enable_cameras``) - - - - Manager Based - - **rl_games** (PPO) - * - Isaac-Cartpole-Showcase-Box-Box-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-Box-Discrete-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-Box-MultiDiscrete-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-Dict-Box-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-Dict-Discrete-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-Dict-MultiDiscrete-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-Discrete-Box-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-Discrete-Discrete-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-Discrete-MultiDiscrete-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-MultiDiscrete-Box-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-MultiDiscrete-Discrete-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-MultiDiscrete-MultiDiscrete-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-Tuple-Box-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-Tuple-Discrete-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-Showcase-Tuple-MultiDiscrete-Direct-v0 - - - - Direct - - **skrl** (PPO) - * - Isaac-Cartpole-v0 - - - - Manager Based - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO), **sb3** (PPO) - * - Isaac-Factory-GearMesh-Direct-v0 - - - - Direct - - **rl_games** (PPO) - * - Isaac-Factory-NutThread-Direct-v0 - - - - Direct - - **rl_games** (PPO) - * - Isaac-Factory-PegInsert-Direct-v0 - - - - Direct - - **rl_games** (PPO) - * - Isaac-AutoMate-Assembly-Direct-v0 - - - - Direct - - **rl_games** (PPO) - * - Isaac-AutoMate-Disassembly-Direct-v0 - - - - Direct - - - * - Isaac-Forge-GearMesh-Direct-v0 - - - - Direct - - **rl_games** (PPO) - * - Isaac-Forge-NutThread-Direct-v0 - - - - Direct - - **rl_games** (PPO) - * - Isaac-Forge-PegInsert-Direct-v0 - - - - Direct - - **rl_games** (PPO) - * - Isaac-Franka-Cabinet-Direct-v0 - - - - Direct - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Humanoid-AMP-Dance-Direct-v0 - - - - Direct - - **skrl** (AMP) - * - Isaac-Humanoid-AMP-Run-Direct-v0 - - - - Direct - - **skrl** (AMP) - * - Isaac-Humanoid-AMP-Walk-Direct-v0 - - - - Direct - - **skrl** (AMP) - * - Isaac-Humanoid-Direct-v0 - - - - Direct - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Humanoid-v0 - - - - Manager Based - - **rsl_rl** (PPO), **rl_games** (PPO), **skrl** (PPO), **sb3** (PPO) - * - Isaac-Lift-Cube-Franka-IK-Abs-v0 - - - - Manager Based - - - * - Isaac-Lift-Cube-Franka-IK-Rel-v0 - - - - Manager Based - - - * - Isaac-Lift-Cube-Franka-v0 - - Isaac-Lift-Cube-Franka-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO), **rl_games** (PPO), **sb3** (PPO) - * - Isaac-Lift-Teddy-Bear-Franka-IK-Abs-v0 - - - - Manager Based - - - * - Isaac-Tracking-LocoManip-Digit-v0 - - Isaac-Tracking-LocoManip-Digit-Play-v0 - - Manager Based - - **rsl_rl** (PPO) - * - Isaac-Navigation-Flat-Anymal-C-v0 - - Isaac-Navigation-Flat-Anymal-C-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Open-Drawer-Franka-IK-Abs-v0 - - - - Manager Based - - - * - Isaac-Open-Drawer-Franka-IK-Rel-v0 - - - - Manager Based - - - * - Isaac-Open-Drawer-Franka-v0 - - Isaac-Open-Drawer-Franka-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **rl_games** (PPO), **skrl** (PPO) - * - Isaac-Quadcopter-Direct-v0 - - - - Direct - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Reach-Franka-IK-Abs-v0 - - - - Manager Based - - - * - Isaac-Reach-Franka-IK-Rel-v0 - - - - Manager Based - - - * - Isaac-Reach-Franka-OSC-v0 - - Isaac-Reach-Franka-OSC-Play-v0 - - Manager Based - - **rsl_rl** (PPO) - * - Isaac-Reach-Franka-v0 - - Isaac-Reach-Franka-Play-v0 - - Manager Based - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Reach-UR10-v0 - - Isaac-Reach-UR10-Play-v0 - - Manager Based - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Deploy-Reach-UR10e-v0 - - Isaac-Deploy-Reach-UR10e-Play-v0 - - Manager Based - - **rsl_rl** (PPO) - * - Isaac-Repose-Cube-Allegro-Direct-v0 - - - - Direct - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Repose-Cube-Allegro-NoVelObs-v0 - - Isaac-Repose-Cube-Allegro-NoVelObs-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **rl_games** (PPO), **skrl** (PPO) - * - Isaac-Repose-Cube-Allegro-v0 - - Isaac-Repose-Cube-Allegro-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **rl_games** (PPO), **skrl** (PPO) - * - Isaac-Repose-Cube-Shadow-Direct-v0 - - - - Direct - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Repose-Cube-Shadow-OpenAI-FF-Direct-v0 - - - - Direct - - **rl_games** (FF), **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Repose-Cube-Shadow-OpenAI-LSTM-Direct-v0 - - - - Direct - - **rl_games** (LSTM) - * - Isaac-Repose-Cube-Shadow-Vision-Direct-v0 (Requires running with ``--enable_cameras``) - - Isaac-Repose-Cube-Shadow-Vision-Direct-Play-v0 (Requires running with ``--enable_cameras``) - - Direct - - **rsl_rl** (PPO), **rl_games** (VISION) - * - Isaac-Shadow-Hand-Over-Direct-v0 - - - - Direct - - **rl_games** (PPO), **skrl** (IPPO, PPO, MAPPO) - * - Isaac-Stack-Cube-Franka-IK-Rel-v0 - - - - Manager Based - - - * - Isaac-Dexsuite-Kuka-Allegro-Lift-v0 - - Isaac-Dexsuite-Kuka-Allegro-Lift-Play-v0 - - Manager Based - - **rl_games** (PPO), **rsl_rl** (PPO) - * - Isaac-Dexsuite-Kuka-Allegro-Reorient-v0 - - Isaac-Dexsuite-Kuka-Allegro-Reorient-Play-v0 - - Manager Based - - **rl_games** (PPO), **rsl_rl** (PPO) - * - Isaac-Stack-Cube-Franka-v0 - - - - Manager Based - - - * - Isaac-Stack-Cube-Instance-Randomize-Franka-IK-Rel-v0 - - - - Manager Based - - - * - Isaac-Stack-Cube-Instance-Randomize-Franka-v0 - - - - Manager Based - - - * - Isaac-PickPlace-G1-InspireFTP-Abs-v0 - - - - Manager Based - - - * - Isaac-Stack-Cube-UR10-Long-Suction-IK-Rel-v0 - - - - Manager Based - - - * - Isaac-Stack-Cube-UR10-Short-Suction-IK-Rel-v0 - - - - Manager Based - - - * - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-v0 - - - - Manager Based - - - * - Isaac-Stack-Cube-Galbot-Right-Arm-Suction-RmpFlow-v0 - - - - Manager Based - - - * - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-v0 - - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-Play-v0 - - Manager Based - - - * - Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-v0 - - - - Manager Based - - - * - Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 - - - - Manager Based - - - * - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-v0 - - - - Manager Based - - - * - Isaac-Stack-Cube-Galbot-Right-Arm-Suction-RmpFlow-v0 - - - - Manager Based - - - * - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-v0 - - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-Play-v0 - - Manager Based - - - * - Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-v0 - - - - Manager Based - - - * - Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 - - - - Manager Based - - - - * - Isaac-Velocity-Flat-Anymal-B-v0 - - Isaac-Velocity-Flat-Anymal-B-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Flat-Anymal-C-Direct-v0 - - - - Direct - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Flat-Anymal-C-v0 - - Isaac-Velocity-Flat-Anymal-C-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **rl_games** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Flat-Anymal-D-v0 - - Isaac-Velocity-Flat-Anymal-D-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Flat-Cassie-v0 - - Isaac-Velocity-Flat-Cassie-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Flat-Digit-v0 - - Isaac-Velocity-Flat-Digit-Play-v0 - - Manager Based - - **rsl_rl** (PPO) - * - Isaac-Velocity-Flat-G1-v0 - - Isaac-Velocity-Flat-G1-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Flat-H1-v0 - - Isaac-Velocity-Flat-H1-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Flat-Spot-v0 - - Isaac-Velocity-Flat-Spot-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Flat-Unitree-A1-v0 - - Isaac-Velocity-Flat-Unitree-A1-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO), **sb3** (PPO) - * - Isaac-Velocity-Flat-Unitree-Go1-v0 - - Isaac-Velocity-Flat-Unitree-Go1-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Flat-Unitree-Go2-v0 - - Isaac-Velocity-Flat-Unitree-Go2-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Rough-Anymal-B-v0 - - Isaac-Velocity-Rough-Anymal-B-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Rough-Anymal-C-Direct-v0 - - - - Direct - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Rough-Anymal-C-v0 - - Isaac-Velocity-Rough-Anymal-C-Play-v0 - - Manager Based - - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Rough-Anymal-D-v0 - - Isaac-Velocity-Rough-Anymal-D-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Rough-Cassie-v0 - - Isaac-Velocity-Rough-Cassie-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Rough-Digit-v0 - - Isaac-Velocity-Rough-Digit-Play-v0 - - Manager Based - - **rsl_rl** (PPO) - * - Isaac-Velocity-Rough-G1-v0 - - Isaac-Velocity-Rough-G1-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Rough-H1-v0 - - Isaac-Velocity-Rough-H1-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Rough-Unitree-A1-v0 - - Isaac-Velocity-Rough-Unitree-A1-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO), **sb3** (PPO) - * - Isaac-Velocity-Rough-Unitree-Go1-v0 - - Isaac-Velocity-Rough-Unitree-Go1-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) - * - Isaac-Velocity-Rough-Unitree-Go2-v0 - - Isaac-Velocity-Rough-Unitree-Go2-Play-v0 - - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) +Use UWLab's ``isaaclab2`` branch or ``v1.3.0`` tag for the Isaac Lab 2.3.2 / +Isaac Sim 5.1 stack. The `Isaac Lab 2.x environment catalogue +`_ +may help identify older task names, but those names and configurations must not +be mixed with the pinned 3.0 stack without migration. diff --git a/docs/source/publications/omnireset/distillation.rst b/docs/source/publications/omnireset/distillation.rst index 9b228ef8..34d3e575 100644 --- a/docs/source/publications/omnireset/distillation.rst +++ b/docs/source/publications/omnireset/distillation.rst @@ -1,6 +1,11 @@ Distillation & Deployment ========================= +.. note:: + + This workflow is expected to work with Isaac Lab 3.0 but has not yet been tested. + Validation is planned; use UWLab ``v1.3.0`` as the legacy reference. + This guide covers distilling a state-based RL expert into a vision-based policy, evaluating it in simulation, and deploying on a real robot. .. _distillation-install: @@ -28,67 +33,7 @@ Then install the dependencies into your UWLab conda environment (required even i cd /diffusion_policy conda activate env_uwlab python -m pip install -e . - python -m pip install dill hydra-core omegaconf zarr einops "diffusers<0.37" wandb accelerate - ----- - -Quick Start: Evaluate Pretrained RGB Policies ----------------------------------------------- - -Download our pretrained vision policy checkpoints and evaluate immediately. All commands in this section run in ``env_uwlab`` from the UWLab directory. - -.. tab-set:: - - .. tab-item:: Peg Insertion - - .. code:: bash - - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/distilled_rgb_policies/peg_distilled_rgb.ckpt - - python scripts_v2/tools/eval_distilled_policy.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-RGB-Play-v0 \ - --checkpoint peg_distilled_rgb.ckpt \ - --num_envs 32 \ - --num_trajectories 100 \ - --headless \ - --enable_cameras \ - --save_video \ - env.scene.insertive_object=peg \ - env.scene.receptive_object=peghole - - .. tab-item:: Leg Twisting - - .. code:: bash - - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/distilled_rgb_policies/leg_distilled_rgb.ckpt - - python scripts_v2/tools/eval_distilled_policy.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-RGB-Play-v0 \ - --checkpoint leg_distilled_rgb.ckpt \ - --num_envs 32 \ - --num_trajectories 100 \ - --headless \ - --enable_cameras \ - --save_video \ - env.scene.insertive_object=fbleg \ - env.scene.receptive_object=fbtabletop - - .. tab-item:: Drawer Assembly - - .. code:: bash - - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/distilled_rgb_policies/drawer_distilled_rgb.ckpt - - python scripts_v2/tools/eval_distilled_policy.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-RGB-Play-v0 \ - --checkpoint drawer_distilled_rgb.ckpt \ - --num_envs 32 \ - --num_trajectories 100 \ - --headless \ - --enable_cameras \ - --save_video \ - env.scene.insertive_object=fbdrawerbottom \ - env.scene.receptive_object=fbdrawerbox + python -m pip install dill hydra-core omegaconf zarr einops "diffusers<0.37" wandb accelerate pandas ---- @@ -104,9 +49,9 @@ To train your own vision policy from scratch, follow the steps below. Collect Demonstrations ^^^^^^^^^^^^^^^^^^^^^^ -**Step 1 — Export the expert policy** +**Step 1: Export the expert policy** -Run ``play.py`` on a **Stage 2** (finetuned) checkpoint to export a JIT-traced ``policy.pt``. You can finetune your own (see :doc:`sim2real`) or download a pre-finetuned checkpoint from the :ref:`finetuned checkpoints ` section. +Run ``play.py`` on a **Stage 2** (finetuned) checkpoint to export a JIT-traced ``policy.pt``. You can finetune your own (see :doc:`sim2real`). .. code:: bash @@ -116,11 +61,11 @@ Run ``play.py`` on a **Stage 2** (finetuned) checkpoint to export a JIT-traced ` --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-Play-v0 \ --num_envs 4 \ --checkpoint \ - --headless + --visualizer none This saves ``policy.pt`` (and ``policy.onnx``) under ``/exported/``. -**Step 2 — Collect RGB demonstrations** +**Step 2: Collect RGB demonstrations** Use the exported ``policy.pt`` to roll out the expert and record RGB observations in Zarr format. Only successful trajectories are saved. @@ -140,8 +85,7 @@ Use the exported ``policy.pt`` to roll out the expert and record RGB observation --dataset_file datasets/peg/rgb0.zarr \ --num_envs 32 \ --num_demos 10000 \ - --enable_cameras \ - --headless \ + --visualizer none \ env.scene.insertive_object=peg \ env.scene.receptive_object=peghole \ agent.algorithm.offline_algorithm_cfg.behavior_cloning_cfg.experts_path='["exported/policy.pt"]' @@ -155,8 +99,7 @@ Use the exported ``policy.pt`` to roll out the expert and record RGB observation --dataset_file datasets/leg/rgb0.zarr \ --num_envs 32 \ --num_demos 10000 \ - --enable_cameras \ - --headless \ + --visualizer none \ env.scene.insertive_object=fbleg \ env.scene.receptive_object=fbtabletop \ agent.algorithm.offline_algorithm_cfg.behavior_cloning_cfg.experts_path='["exported/policy.pt"]' @@ -170,8 +113,7 @@ Use the exported ``policy.pt`` to roll out the expert and record RGB observation --dataset_file datasets/drawer/rgb0.zarr \ --num_envs 32 \ --num_demos 10000 \ - --enable_cameras \ - --headless \ + --visualizer none \ env.scene.insertive_object=fbdrawerbottom \ env.scene.receptive_object=fbdrawerbox \ agent.algorithm.offline_algorithm_cfg.behavior_cloning_cfg.experts_path='["exported/policy.pt"]' @@ -259,8 +201,7 @@ Evaluate the trained vision policy in simulation. All commands below run in ``en --checkpoint .ckpt \ --num_envs 32 \ --num_trajectories 100 \ - --headless \ - --enable_cameras \ + --visualizer none \ --save_video \ env.scene.insertive_object=peg \ env.scene.receptive_object=peghole @@ -274,8 +215,7 @@ Evaluate the trained vision policy in simulation. All commands below run in ``en --checkpoint .ckpt \ --num_envs 32 \ --num_trajectories 100 \ - --headless \ - --enable_cameras \ + --visualizer none \ env.scene.insertive_object=peg \ env.scene.receptive_object=peghole @@ -290,8 +230,7 @@ Evaluate the trained vision policy in simulation. All commands below run in ``en --checkpoint .ckpt \ --num_envs 32 \ --num_trajectories 100 \ - --headless \ - --enable_cameras \ + --visualizer none \ --save_video \ env.scene.insertive_object=fbleg \ env.scene.receptive_object=fbtabletop @@ -305,8 +244,7 @@ Evaluate the trained vision policy in simulation. All commands below run in ``en --checkpoint .ckpt \ --num_envs 32 \ --num_trajectories 100 \ - --headless \ - --enable_cameras \ + --visualizer none \ env.scene.insertive_object=fbleg \ env.scene.receptive_object=fbtabletop @@ -321,8 +259,7 @@ Evaluate the trained vision policy in simulation. All commands below run in ``en --checkpoint .ckpt \ --num_envs 32 \ --num_trajectories 100 \ - --headless \ - --enable_cameras \ + --visualizer none \ --save_video \ env.scene.insertive_object=fbdrawerbottom \ env.scene.receptive_object=fbdrawerbox @@ -336,8 +273,7 @@ Evaluate the trained vision policy in simulation. All commands below run in ``en --checkpoint .ckpt \ --num_envs 32 \ --num_trajectories 100 \ - --headless \ - --enable_cameras \ + --visualizer none \ env.scene.insertive_object=fbdrawerbottom \ env.scene.receptive_object=fbdrawerbox diff --git a/docs/source/publications/omnireset/index.rst b/docs/source/publications/omnireset/index.rst index 7dc3f96d..40314739 100644 --- a/docs/source/publications/omnireset/index.rst +++ b/docs/source/publications/omnireset/index.rst @@ -36,7 +36,7 @@ Download our pretrained checkpoint and run evaluation. .. code:: bash - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts/leg_state_rl_expert_seed42.pt + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/leg_state_rl_expert_seed42.pt python scripts/reinforcement_learning/rsl_rl/play.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ @@ -45,29 +45,29 @@ Download our pretrained checkpoint and run evaluation. env.scene.insertive_object=fbleg \ env.scene.receptive_object=fbtabletop - .. tab-item:: Seed 0 + .. tab-item:: Seed 43 .. code:: bash - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts/leg_state_rl_expert_seed0.pt + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/leg_state_rl_expert_seed43.pt python scripts/reinforcement_learning/rsl_rl/play.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ --num_envs 1 \ - --checkpoint leg_state_rl_expert_seed0.pt \ + --checkpoint leg_state_rl_expert_seed43.pt \ env.scene.insertive_object=fbleg \ env.scene.receptive_object=fbtabletop - .. tab-item:: Seed 1 + .. tab-item:: Seed 44 .. code:: bash - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts/leg_state_rl_expert_seed1.pt + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/leg_state_rl_expert_seed44.pt python scripts/reinforcement_learning/rsl_rl/play.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ --num_envs 1 \ - --checkpoint leg_state_rl_expert_seed1.pt \ + --checkpoint leg_state_rl_expert_seed44.pt \ env.scene.insertive_object=fbleg \ env.scene.receptive_object=fbtabletop @@ -88,7 +88,7 @@ Download our pretrained checkpoint and run evaluation. .. code:: bash - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts/drawer_state_rl_expert_seed42.pt + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/drawer_state_rl_expert_seed42.pt python scripts/reinforcement_learning/rsl_rl/play.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ @@ -97,29 +97,29 @@ Download our pretrained checkpoint and run evaluation. env.scene.insertive_object=fbdrawerbottom \ env.scene.receptive_object=fbdrawerbox - .. tab-item:: Seed 0 + .. tab-item:: Seed 43 .. code:: bash - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts/drawer_state_rl_expert_seed0.pt + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/drawer_state_rl_expert_seed43.pt python scripts/reinforcement_learning/rsl_rl/play.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ --num_envs 1 \ - --checkpoint drawer_state_rl_expert_seed0.pt \ + --checkpoint drawer_state_rl_expert_seed43.pt \ env.scene.insertive_object=fbdrawerbottom \ env.scene.receptive_object=fbdrawerbox - .. tab-item:: Seed 1 + .. tab-item:: Seed 44 .. code:: bash - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts/drawer_state_rl_expert_seed1.pt + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/drawer_state_rl_expert_seed44.pt python scripts/reinforcement_learning/rsl_rl/play.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ --num_envs 1 \ - --checkpoint drawer_state_rl_expert_seed1.pt \ + --checkpoint drawer_state_rl_expert_seed44.pt \ env.scene.insertive_object=fbdrawerbottom \ env.scene.receptive_object=fbdrawerbox @@ -140,7 +140,7 @@ Download our pretrained checkpoint and run evaluation. .. code:: bash - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts/peg_state_rl_expert_seed42.pt + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/peg_state_rl_expert_seed42.pt python scripts/reinforcement_learning/rsl_rl/play.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ @@ -149,29 +149,29 @@ Download our pretrained checkpoint and run evaluation. env.scene.insertive_object=peg \ env.scene.receptive_object=peghole - .. tab-item:: Seed 0 + .. tab-item:: Seed 43 .. code:: bash - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts/peg_state_rl_expert_seed0.pt + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/peg_state_rl_expert_seed43.pt python scripts/reinforcement_learning/rsl_rl/play.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ --num_envs 1 \ - --checkpoint peg_state_rl_expert_seed0.pt \ + --checkpoint peg_state_rl_expert_seed43.pt \ env.scene.insertive_object=peg \ env.scene.receptive_object=peghole - .. tab-item:: Seed 1 + .. tab-item:: Seed 44 .. code:: bash - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts/peg_state_rl_expert_seed1.pt + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/peg_state_rl_expert_seed44.pt python scripts/reinforcement_learning/rsl_rl/play.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ --num_envs 1 \ - --checkpoint peg_state_rl_expert_seed1.pt \ + --checkpoint peg_state_rl_expert_seed44.pt \ env.scene.insertive_object=peg \ env.scene.receptive_object=peghole @@ -186,18 +186,53 @@ Download our pretrained checkpoint and run evaluation. - .. code:: bash + .. warning:: - # Download checkpoint - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts/rectangle_state_rl_expert_seed0.pt + **Isaac Lab 3 regression:** Rectangle success stalls at 62-65%. + For now, use `UWLab v1.2.0 `_ + with Isaac Lab pinned to 2.x. Have a fix? + `Send a PR `_. We'll take a look. - # Run evaluation - python scripts/reinforcement_learning/rsl_rl/play.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 1 \ - --checkpoint rectangle_state_rl_expert_seed0.pt \ - env.scene.insertive_object=rectangle \ - env.scene.receptive_object=wall + .. tab-set:: + + .. tab-item:: Seed 42 + + .. code:: bash + + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/rectangle_state_rl_expert_seed42.pt + + python scripts/reinforcement_learning/rsl_rl/play.py \ + --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ + --num_envs 1 \ + --checkpoint rectangle_state_rl_expert_seed42.pt \ + env.scene.insertive_object=rectangle \ + env.scene.receptive_object=wall + + .. tab-item:: Seed 43 + + .. code:: bash + + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/rectangle_state_rl_expert_seed43.pt + + python scripts/reinforcement_learning/rsl_rl/play.py \ + --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ + --num_envs 1 \ + --checkpoint rectangle_state_rl_expert_seed43.pt \ + env.scene.insertive_object=rectangle \ + env.scene.receptive_object=wall + + .. tab-item:: Seed 44 + + .. code:: bash + + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/rectangle_state_rl_expert_seed44.pt + + python scripts/reinforcement_learning/rsl_rl/play.py \ + --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ + --num_envs 1 \ + --checkpoint rectangle_state_rl_expert_seed44.pt \ + env.scene.insertive_object=rectangle \ + env.scene.receptive_object=wall .. tab-item:: Cube Stacking @@ -210,18 +245,46 @@ Download our pretrained checkpoint and run evaluation. - .. code:: bash + .. tab-set:: + + .. tab-item:: Seed 42 + + .. code:: bash + + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/cube_state_rl_expert_seed42.pt + + python scripts/reinforcement_learning/rsl_rl/play.py \ + --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ + --num_envs 1 \ + --checkpoint cube_state_rl_expert_seed42.pt \ + env.scene.insertive_object=cube \ + env.scene.receptive_object=cube + + .. tab-item:: Seed 43 - # Download checkpoint - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts/cube_state_rl_expert_seed42.pt + .. code:: bash + + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/cube_state_rl_expert_seed43.pt + + python scripts/reinforcement_learning/rsl_rl/play.py \ + --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ + --num_envs 1 \ + --checkpoint cube_state_rl_expert_seed43.pt \ + env.scene.insertive_object=cube \ + env.scene.receptive_object=cube + + .. tab-item:: Seed 44 - # Run evaluation - python scripts/reinforcement_learning/rsl_rl/play.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 1 \ - --checkpoint cube_state_rl_expert_seed42.pt \ - env.scene.insertive_object=cube \ - env.scene.receptive_object=cube + .. code:: bash + + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/cube_state_rl_expert_seed44.pt + + python scripts/reinforcement_learning/rsl_rl/play.py \ + --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ + --num_envs 1 \ + --checkpoint cube_state_rl_expert_seed44.pt \ + env.scene.insertive_object=cube \ + env.scene.receptive_object=cube .. tab-item:: Cupcake on Plate @@ -234,18 +297,46 @@ Download our pretrained checkpoint and run evaluation. - .. code:: bash + .. tab-set:: - # Download checkpoint - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts/cupcake_state_rl_expert_seed42.pt + .. tab-item:: Seed 42 - # Run evaluation - python scripts/reinforcement_learning/rsl_rl/play.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 1 \ - --checkpoint cupcake_state_rl_expert_seed42.pt \ - env.scene.insertive_object=cupcake \ - env.scene.receptive_object=plate + .. code:: bash + + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/cupcake_state_rl_expert_seed42.pt + + python scripts/reinforcement_learning/rsl_rl/play.py \ + --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ + --num_envs 1 \ + --checkpoint cupcake_state_rl_expert_seed42.pt \ + env.scene.insertive_object=cupcake \ + env.scene.receptive_object=plate + + .. tab-item:: Seed 43 + + .. code:: bash + + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/cupcake_state_rl_expert_seed43.pt + + python scripts/reinforcement_learning/rsl_rl/play.py \ + --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ + --num_envs 1 \ + --checkpoint cupcake_state_rl_expert_seed43.pt \ + env.scene.insertive_object=cupcake \ + env.scene.receptive_object=plate + + .. tab-item:: Seed 44 + + .. code:: bash + + wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/83860532010b2737aa80d6e8621235ee554186f0/Policies/OmniReset/state_based_experts/cupcake_state_rl_expert_seed44.pt + + python scripts/reinforcement_learning/rsl_rl/play.py \ + --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ + --num_envs 1 \ + --checkpoint cupcake_state_rl_expert_seed44.pt \ + env.scene.insertive_object=cupcake \ + env.scene.receptive_object=plate ---- @@ -282,8 +373,8 @@ The full OmniReset pipeline from custom task creation to real-robot deployment: - :doc:`new_task` -- Prepare USD assets, register object variants, verify in sim. - :doc:`rl_training` -- Collect reset states and train an RL policy from scratch. **Start here for most use cases.** -- :doc:`sim2real` -- Robot calibration & USD, system identification, camera calibration, then ADR finetuning, or use our pre-finetuned checkpoints. -- :doc:`distillation` -- Evaluate pretrained RGB checkpoints, or collect demos and train your own ResNet18-MLP vision policy. Deploy on real robot. +- :doc:`sim2real` -- Robot calibration & USD, system identification, camera calibration, then ADR finetuning. +- :doc:`distillation` -- Collect demos and train your own ResNet18-MLP vision policy. Deploy on real robot. .. toctree:: :maxdepth: 1 diff --git a/docs/source/publications/omnireset/new_task.rst b/docs/source/publications/omnireset/new_task.rst index 7fcf6389..a64033a9 100644 --- a/docs/source/publications/omnireset/new_task.rst +++ b/docs/source/publications/omnireset/new_task.rst @@ -16,6 +16,8 @@ In a single Blender session: 2. Reorient so Z-axis points up when the object is resting on a table: ``Tab`` > Edit Mode, ``A`` to select all, rotate as needed (e.g. ``R X 90``). 3. Set origin: right-click > Set Origin > Origin to Center of Mass (Volume). 4. Place both objects in assembled pose, record the relative transform for ``assembled_pose`` to be used in Step 4. + Write its rotation as a quaternion in ``(x, y, z, w)`` order: Isaac Lab 3.0 changed the convention from + 2.x's ``(w, x, y, z)``, so a quaternion written the old way is a different rotation, not an error. 5. Export each object as ``.usdz``. .. raw:: html @@ -89,6 +91,9 @@ The metadata has the following fields: - ``assembled_offset``: Transform from the insertive object to this object in the assembled pose. Always identity for the insertive object; for the receptive object, use the relative transform recorded in Step 1. - ``bottom_offset``: Transform from origin to the bottom of the object. The Z value is the **negative** of the script output from Step 3. +- ``quat_convention``: Must be ``xyzw``. Since the Isaac Lab 3.0 bump, quaternions are ``(x, y, z, w)`` + (identity is ``[0.0, 0.0, 0.0, 1.0]``, not ``[1.0, 0.0, 0.0, 0.0]``). UW Lab refuses metadata without this + key rather than silently applying a rotated offset. - ``success_thresholds`` (receptive only): How tightly the policy must align parts. Use ``position: 0.0025, orientation: 0.025`` for tight-fit tasks (e.g. screw insertion). For looser tasks (e.g. cube stacking), try ``position: 0.005, orientation: 0.05``. May need to tune depending on the task. **Insertive object** example: @@ -97,10 +102,11 @@ The metadata has the following fields: assembled_offset: pos: [0.0, 0.0, 0.0] - quat: [1.0, 0.0, 0.0, 0.0] + quat: [0.0, 0.0, 0.0, 1.0] bottom_offset: pos: [0.0, 0.0, -0.056658] - quat: [1.0, 0.0, 0.0, 0.0] + quat: [0.0, 0.0, 0.0, 1.0] + quat_convention: xyzw **Receptive object** example: @@ -108,13 +114,14 @@ The metadata has the following fields: assembled_offset: pos: [0.012, 0.0, 0.035] - quat: [1.0, 0.0, 0.0, 0.0] + quat: [0.0, 0.0, 0.0, 1.0] bottom_offset: pos: [0.0, 0.0, -0.010169] - quat: [1.0, 0.0, 0.0, 0.0] + quat: [0.0, 0.0, 0.0, 1.0] success_thresholds: position: 0.0025 orientation: 0.025 + quat_convention: xyzw ---- @@ -164,6 +171,9 @@ Add to ``variants["scene.receptive_object"]``: .. tip:: Use local absolute paths during development. Switch to ``UWLAB_CLOUD_ASSETS_DIR`` when sharing. + Publish Isaac Lab 3.0 / Isaac Sim 6.1 assets on the ``isaaclab3`` branch of the asset repository + (``main`` holds the Isaac Lab 2.x files), then pin ``uwlab_assets.UWLAB_CLOUD_ASSETS_REVISION`` + to a commit that contains them. ---- @@ -176,7 +186,7 @@ Step 6: Verify Setup python scripts_v2/tools/record_partial_assemblies.py \ --task OmniReset-PartialAssemblies-v0 \ - --num_envs 10 --num_trajectories 10 --headless \ + --num_envs 10 --num_trajectories 10 --visualizer none \ env.scene.insertive_object=my_insertive_object env.scene.receptive_object=my_receptive_object If objects are misaligned or upside down, revisit Step 1. @@ -187,14 +197,14 @@ If objects are misaligned or upside down, revisit Step 1. python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEAnywhere-v0 \ - --num_envs 4 --num_reset_states 8 --headless \ + --num_envs 4 --num_reset_states 8 --visualizer none \ env.scene.insertive_object=my_insertive_object env.scene.receptive_object=my_receptive_object .. code:: bash python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ env.scene.insertive_object=my_insertive_object env.scene.receptive_object=my_receptive_object Confirm the receptive object sits flush on the table. If it's floating or clipping, adjust the ``bottom_offset`` in Step 4. diff --git a/docs/source/publications/omnireset/rl_training.rst b/docs/source/publications/omnireset/rl_training.rst index 597331af..6888ff93 100644 --- a/docs/source/publications/omnireset/rl_training.rst +++ b/docs/source/publications/omnireset/rl_training.rst @@ -9,6 +9,26 @@ Reproduce our training results from scratch. **Want to try it quickly?** Start with **Cube Stacking** or **Peg Insertion**. They have the fastest reset state collection times and converge within ~8 hours on 4×L40S GPUs. +Plots show seeds 42, 43, and 44 on 4x L40S each. Relaunches are joined by PPO update; +dotted links mark gaps without logged samples. The first 30 updates after a resume are +omitted while the success window refills. Time sums the logged runtime of each run, +not total elapsed wall-clock time. + +.. warning:: + + **Known performance regressions in Isaac Lab 3** + + * **Leg Twisting:** Training success can drop sharply after roughly 24 hours. + The current workaround is to restart training from a checkpoint saved **before the drop**, + rather than from the latest degraded checkpoint. This is a workaround, not a resolved fix. + * **Rectangle on Wall:** Performance has degraded significantly compared with the Isaac Lab 2 + version. If you need this task now, use + `UWLab v1.2.0 `_ with an **Isaac Lab 2.x** environment, + rather than mixing that tag with the current Isaac Lab 3 installation. + + If you have a fix for either issue, please + `open a pull request `_. We'll review it. + .. tab-set:: .. tab-item:: Leg Twisting @@ -21,13 +41,13 @@ Reproduce our training results from scratch. .. code:: bash - python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --headless env.scene.insertive_object=fbleg env.scene.receptive_object=fbtabletop + python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --visualizer none env.scene.insertive_object=fbleg env.scene.receptive_object=fbtabletop **Step 2: Sample Grasp Poses** (~1 minute) .. code:: bash - python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --headless env.scene.object=fbleg + python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --visualizer none env.scene.object=fbleg **Step 3: Generate Reset State Datasets** (~1 min to multiple hours depending on the reset and task) @@ -36,31 +56,31 @@ Reproduce our training results from scratch. # Object Anywhere, End-Effector Anywhere (Reaching) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEAnywhere-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=fbleg env.scene.receptive_object=fbtabletop # Object Resting, End-Effector Grasped (Near Object) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectRestingEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=fbleg env.scene.receptive_object=fbtabletop \ - env.events.reset_insertive_object_pose_from_reset_states.params.dataset_dir=./Datasets/OmniReset \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_insertive_object_pose_from_reset_states.params.dataset_dir=./Datasets/OmniReset_isaaclab3 \ + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 # Object Anywhere, End-Effector Grasped (Grasped) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=fbleg env.scene.receptive_object=fbtabletop \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 # Object Partially Assembled, End-Effector Grasped (Near Goal) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectPartiallyAssembledEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=fbleg env.scene.receptive_object=fbtabletop \ - env.events.reset_insertive_object_pose_from_partial_assembly_dataset.params.dataset_dir=./Datasets/OmniReset \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_insertive_object_pose_from_partial_assembly_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 \ + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 **Step 3.5: Visualize Reset States (Optional)** @@ -74,7 +94,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ env.scene.insertive_object=fbleg env.scene.receptive_object=fbtabletop .. tab-item:: Reaching @@ -83,7 +103,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectAnywhereEEAnywhere \ env.scene.insertive_object=fbleg env.scene.receptive_object=fbtabletop @@ -93,7 +113,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectRestingEEGrasped \ env.scene.insertive_object=fbleg env.scene.receptive_object=fbtabletop @@ -103,7 +123,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectAnywhereEEGrasped \ env.scene.insertive_object=fbleg env.scene.receptive_object=fbtabletop @@ -113,7 +133,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectPartiallyAssembledEEGrasped \ env.scene.insertive_object=fbleg env.scene.receptive_object=fbtabletop @@ -130,7 +150,7 @@ Reproduce our training results from scratch. --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ env.scene.insertive_object=fbleg \ env.scene.receptive_object=fbtabletop @@ -146,25 +166,27 @@ Reproduce our training results from scratch. --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ env.scene.insertive_object=fbleg \ env.scene.receptive_object=fbtabletop \ - env.events.reset_from_reset_states.params.dataset_dir=./Datasets/OmniReset + env.events.reset_from_reset_states.params.dataset_dir=./Datasets/OmniReset_isaaclab3 **Training Curves** + Shown through **22.5 logged hours**, before the first seed drops. + .. list-table:: :widths: 50 50 :class: borderless * - .. figure:: ../../../source/_static/publications/omnireset/leg_success_rate_seeds.jpg :width: 100% - :alt: Leg twisting success rate over steps + :alt: Leg twisting success rate over PPO updates - .. figure:: ../../../source/_static/publications/omnireset/leg_success_rate_seeds_walltime.jpg :width: 100% - :alt: Leg twisting success rate over wall clock time + :alt: Leg twisting success rate over cumulative logged runtime .. tab-item:: Drawer Assembly @@ -176,13 +198,13 @@ Reproduce our training results from scratch. .. code:: bash - python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --headless env.scene.insertive_object=fbdrawerbottom env.scene.receptive_object=fbdrawerbox + python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --visualizer none env.scene.insertive_object=fbdrawerbottom env.scene.receptive_object=fbdrawerbox **Step 2: Sample Grasp Poses** (~1 minute) .. code:: bash - python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --headless env.scene.object=fbdrawerbottom + python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --visualizer none env.scene.object=fbdrawerbottom **Step 3: Generate Reset State Datasets** (~1 min to multiple hours depending on the reset and task) @@ -191,31 +213,31 @@ Reproduce our training results from scratch. # Object Anywhere, End-Effector Anywhere (Reaching) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEAnywhere-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=fbdrawerbottom env.scene.receptive_object=fbdrawerbox # Object Resting, End-Effector Grasped (Near Object) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectRestingEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=fbdrawerbottom env.scene.receptive_object=fbdrawerbox \ - env.events.reset_insertive_object_pose_from_reset_states.params.dataset_dir=./Datasets/OmniReset \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_insertive_object_pose_from_reset_states.params.dataset_dir=./Datasets/OmniReset_isaaclab3 \ + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 # Object Anywhere, End-Effector Grasped (Grasped) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=fbdrawerbottom env.scene.receptive_object=fbdrawerbox \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 # Object Partially Assembled, End-Effector Grasped (Near Goal) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectPartiallyAssembledEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=fbdrawerbottom env.scene.receptive_object=fbdrawerbox \ - env.events.reset_insertive_object_pose_from_partial_assembly_dataset.params.dataset_dir=./Datasets/OmniReset \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_insertive_object_pose_from_partial_assembly_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 \ + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 **Step 3.5: Visualize Reset States (Optional)** @@ -229,7 +251,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ env.scene.insertive_object=fbdrawerbottom env.scene.receptive_object=fbdrawerbox .. tab-item:: Reaching @@ -238,7 +260,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectAnywhereEEAnywhere \ env.scene.insertive_object=fbdrawerbottom env.scene.receptive_object=fbdrawerbox @@ -248,7 +270,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectRestingEEGrasped \ env.scene.insertive_object=fbdrawerbottom env.scene.receptive_object=fbdrawerbox @@ -258,7 +280,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectAnywhereEEGrasped \ env.scene.insertive_object=fbdrawerbottom env.scene.receptive_object=fbdrawerbox @@ -268,7 +290,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectPartiallyAssembledEEGrasped \ env.scene.insertive_object=fbdrawerbottom env.scene.receptive_object=fbdrawerbox @@ -285,7 +307,7 @@ Reproduce our training results from scratch. --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ env.scene.insertive_object=fbdrawerbottom \ env.scene.receptive_object=fbdrawerbox @@ -301,11 +323,11 @@ Reproduce our training results from scratch. --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ env.scene.insertive_object=fbdrawerbottom \ env.scene.receptive_object=fbdrawerbox \ - env.events.reset_from_reset_states.params.dataset_dir=./Datasets/OmniReset + env.events.reset_from_reset_states.params.dataset_dir=./Datasets/OmniReset_isaaclab3 **Training Curves** @@ -315,11 +337,11 @@ Reproduce our training results from scratch. * - .. figure:: ../../../source/_static/publications/omnireset/drawer_success_rate_seeds.jpg :width: 100% - :alt: Drawer assembly success rate over steps + :alt: Drawer assembly success rate over PPO updates - .. figure:: ../../../source/_static/publications/omnireset/drawer_success_rate_seeds_walltime.jpg :width: 100% - :alt: Drawer assembly success rate over wall clock time + :alt: Drawer assembly success rate over cumulative logged runtime .. tab-item:: Peg Insertion @@ -331,13 +353,13 @@ Reproduce our training results from scratch. .. code:: bash - python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --headless env.scene.insertive_object=peg env.scene.receptive_object=peghole + python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --visualizer none env.scene.insertive_object=peg env.scene.receptive_object=peghole **Step 2: Sample Grasp Poses** (~1 minute) .. code:: bash - python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --headless env.scene.object=peg + python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --visualizer none env.scene.object=peg **Step 3: Generate Reset State Datasets** (~1 min to multiple hours depending on the reset and task) @@ -346,31 +368,31 @@ Reproduce our training results from scratch. # Object Anywhere, End-Effector Anywhere (Reaching) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEAnywhere-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=peg env.scene.receptive_object=peghole # Object Resting, End-Effector Grasped (Near Object) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectRestingEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=peg env.scene.receptive_object=peghole \ - env.events.reset_insertive_object_pose_from_reset_states.params.dataset_dir=./Datasets/OmniReset \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_insertive_object_pose_from_reset_states.params.dataset_dir=./Datasets/OmniReset_isaaclab3 \ + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 # Object Anywhere, End-Effector Grasped (Grasped) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=peg env.scene.receptive_object=peghole \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 # Object Partially Assembled, End-Effector Grasped (Near Goal) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectPartiallyAssembledEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=peg env.scene.receptive_object=peghole \ - env.events.reset_insertive_object_pose_from_partial_assembly_dataset.params.dataset_dir=./Datasets/OmniReset \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_insertive_object_pose_from_partial_assembly_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 \ + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 **Step 3.5: Visualize Reset States (Optional)** @@ -384,7 +406,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ env.scene.insertive_object=peg env.scene.receptive_object=peghole .. tab-item:: Reaching @@ -393,7 +415,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectAnywhereEEAnywhere \ env.scene.insertive_object=peg env.scene.receptive_object=peghole @@ -403,7 +425,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectRestingEEGrasped \ env.scene.insertive_object=peg env.scene.receptive_object=peghole @@ -413,7 +435,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectAnywhereEEGrasped \ env.scene.insertive_object=peg env.scene.receptive_object=peghole @@ -423,7 +445,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectPartiallyAssembledEEGrasped \ env.scene.insertive_object=peg env.scene.receptive_object=peghole @@ -440,7 +462,7 @@ Reproduce our training results from scratch. --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ env.scene.insertive_object=peg \ env.scene.receptive_object=peghole @@ -456,11 +478,11 @@ Reproduce our training results from scratch. --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ env.scene.insertive_object=peg \ env.scene.receptive_object=peghole \ - env.events.reset_from_reset_states.params.dataset_dir=./Datasets/OmniReset + env.events.reset_from_reset_states.params.dataset_dir=./Datasets/OmniReset_isaaclab3 **Training Curves** @@ -470,14 +492,21 @@ Reproduce our training results from scratch. * - .. figure:: ../../../source/_static/publications/omnireset/peg_success_rate_seeds.jpg :width: 100% - :alt: Peg insertion success rate over steps + :alt: Peg insertion success rate over PPO updates - .. figure:: ../../../source/_static/publications/omnireset/peg_success_rate_seeds_walltime.jpg :width: 100% - :alt: Peg insertion success rate over wall clock time + :alt: Peg insertion success rate over cumulative logged runtime .. tab-item:: Rectangle on Wall + .. warning:: + + **Isaac Lab 3 regression:** Rectangle success stalls at 62-65%. + For now, use `UWLab v1.2.0 `_ + with Isaac Lab pinned to 2.x. Have a fix? + `Send a PR `_. We'll take a look. + .. note:: **Skip directly to Step 4** if you want to train an RL policy with our pre-generated reset state datasets. Only run Steps 1-3 if you want to generate your own. @@ -486,13 +515,13 @@ Reproduce our training results from scratch. .. code:: bash - python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --headless env.scene.insertive_object=rectangle env.scene.receptive_object=wall + python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --visualizer none env.scene.insertive_object=rectangle env.scene.receptive_object=wall **Step 2: Sample Grasp Poses** (~1 minute) .. code:: bash - python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --headless env.scene.object=rectangle + python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --visualizer none env.scene.object=rectangle **Step 3: Generate Reset State Datasets** (~1 min to multiple hours depending on the reset and task) @@ -501,31 +530,31 @@ Reproduce our training results from scratch. # Object Anywhere, End-Effector Anywhere (Reaching) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEAnywhere-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=rectangle env.scene.receptive_object=wall # Object Resting, End-Effector Grasped (Near Object) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectRestingEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=rectangle env.scene.receptive_object=wall \ - env.events.reset_insertive_object_pose_from_reset_states.params.dataset_dir=./Datasets/OmniReset \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_insertive_object_pose_from_reset_states.params.dataset_dir=./Datasets/OmniReset_isaaclab3 \ + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 # Object Anywhere, End-Effector Grasped (Grasped) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=rectangle env.scene.receptive_object=wall \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 # Object Partially Assembled, End-Effector Grasped (Near Goal) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectPartiallyAssembledEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=rectangle env.scene.receptive_object=wall \ - env.events.reset_insertive_object_pose_from_partial_assembly_dataset.params.dataset_dir=./Datasets/OmniReset \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_insertive_object_pose_from_partial_assembly_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 \ + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 **Step 3.5: Visualize Reset States (Optional)** @@ -539,7 +568,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ env.scene.insertive_object=rectangle env.scene.receptive_object=wall .. tab-item:: Reaching @@ -548,7 +577,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectAnywhereEEAnywhere \ env.scene.insertive_object=rectangle env.scene.receptive_object=wall @@ -558,7 +587,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectRestingEEGrasped \ env.scene.insertive_object=rectangle env.scene.receptive_object=wall @@ -568,7 +597,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectAnywhereEEGrasped \ env.scene.insertive_object=rectangle env.scene.receptive_object=wall @@ -578,7 +607,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectPartiallyAssembledEEGrasped \ env.scene.insertive_object=rectangle env.scene.receptive_object=wall @@ -595,7 +624,7 @@ Reproduce our training results from scratch. --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ env.scene.insertive_object=rectangle \ env.scene.receptive_object=wall @@ -611,15 +640,11 @@ Reproduce our training results from scratch. --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ env.scene.insertive_object=rectangle \ env.scene.receptive_object=wall \ - env.events.reset_from_reset_states.params.dataset_dir=./Datasets/OmniReset - - .. warning:: - - This task has the least stable training. Some seeds plateau around 60%; if a run dies, reload from a checkpoint before the crash. You may need to try a few seeds (plot below is seed 0). + env.events.reset_from_reset_states.params.dataset_dir=./Datasets/OmniReset_isaaclab3 **Training Curves** @@ -629,11 +654,11 @@ Reproduce our training results from scratch. * - .. figure:: ../../../source/_static/publications/omnireset/rectangle_success_rate_seeds.jpg :width: 100% - :alt: Rectangle on wall success rate over steps + :alt: Rectangle on wall success rate over PPO updates - .. figure:: ../../../source/_static/publications/omnireset/rectangle_success_rate_seeds_walltime.jpg :width: 100% - :alt: Rectangle on wall success rate over wall clock time + :alt: Rectangle on wall success rate over cumulative logged runtime .. tab-item:: Cube Stacking @@ -645,13 +670,13 @@ Reproduce our training results from scratch. .. code:: bash - python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --headless env.scene.insertive_object=cube env.scene.receptive_object=cube + python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --visualizer none env.scene.insertive_object=cube env.scene.receptive_object=cube **Step 2: Sample Grasp Poses** (~1 minute) .. code:: bash - python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --headless env.scene.object=cube + python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --visualizer none env.scene.object=cube **Step 3: Generate Reset State Datasets** (~1 min to multiple hours depending on the reset and task) @@ -660,31 +685,31 @@ Reproduce our training results from scratch. # Object Anywhere, End-Effector Anywhere (Reaching) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEAnywhere-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=cube env.scene.receptive_object=cube # Object Resting, End-Effector Grasped (Near Object) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectRestingEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=cube env.scene.receptive_object=cube \ - env.events.reset_insertive_object_pose_from_reset_states.params.dataset_dir=./Datasets/OmniReset \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_insertive_object_pose_from_reset_states.params.dataset_dir=./Datasets/OmniReset_isaaclab3 \ + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 # Object Anywhere, End-Effector Grasped (Grasped) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=cube env.scene.receptive_object=cube \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 # Object Partially Assembled, End-Effector Grasped (Near Goal) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectPartiallyAssembledEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=cube env.scene.receptive_object=cube \ - env.events.reset_insertive_object_pose_from_partial_assembly_dataset.params.dataset_dir=./Datasets/OmniReset \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_insertive_object_pose_from_partial_assembly_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 \ + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 **Step 3.5: Visualize Reset States (Optional)** @@ -698,7 +723,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ env.scene.insertive_object=cube env.scene.receptive_object=cube .. tab-item:: Reaching @@ -707,7 +732,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectAnywhereEEAnywhere \ env.scene.insertive_object=cube env.scene.receptive_object=cube @@ -717,7 +742,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectRestingEEGrasped \ env.scene.insertive_object=cube env.scene.receptive_object=cube @@ -727,7 +752,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectAnywhereEEGrasped \ env.scene.insertive_object=cube env.scene.receptive_object=cube @@ -737,7 +762,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectPartiallyAssembledEEGrasped \ env.scene.insertive_object=cube env.scene.receptive_object=cube @@ -754,7 +779,7 @@ Reproduce our training results from scratch. --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ env.scene.insertive_object=cube \ env.scene.receptive_object=cube @@ -770,11 +795,11 @@ Reproduce our training results from scratch. --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ env.scene.insertive_object=cube \ env.scene.receptive_object=cube \ - env.events.reset_from_reset_states.params.dataset_dir=./Datasets/OmniReset + env.events.reset_from_reset_states.params.dataset_dir=./Datasets/OmniReset_isaaclab3 **Training Curves** @@ -784,11 +809,11 @@ Reproduce our training results from scratch. * - .. figure:: ../../../source/_static/publications/omnireset/cube_success_rate_seeds.jpg :width: 100% - :alt: Cube stacking success rate over steps + :alt: Cube stacking success rate over PPO updates - .. figure:: ../../../source/_static/publications/omnireset/cube_success_rate_seeds_walltime.jpg :width: 100% - :alt: Cube stacking success rate over wall clock time + :alt: Cube stacking success rate over cumulative logged runtime .. tab-item:: Cupcake on Plate @@ -800,13 +825,13 @@ Reproduce our training results from scratch. .. code:: bash - python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --headless env.scene.insertive_object=cupcake env.scene.receptive_object=plate + python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --visualizer none env.scene.insertive_object=cupcake env.scene.receptive_object=plate **Step 2: Sample Grasp Poses** (~1 minute) .. code:: bash - python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --headless env.scene.object=cupcake + python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --visualizer none env.scene.object=cupcake **Step 3: Generate Reset State Datasets** (~1 min to multiple hours depending on the reset and task) @@ -815,31 +840,31 @@ Reproduce our training results from scratch. # Object Anywhere, End-Effector Anywhere (Reaching) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEAnywhere-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=cupcake env.scene.receptive_object=plate # Object Resting, End-Effector Grasped (Near Object) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectRestingEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=cupcake env.scene.receptive_object=plate \ - env.events.reset_insertive_object_pose_from_reset_states.params.dataset_dir=./Datasets/OmniReset \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_insertive_object_pose_from_reset_states.params.dataset_dir=./Datasets/OmniReset_isaaclab3 \ + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 # Object Anywhere, End-Effector Grasped (Grasped) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=cupcake env.scene.receptive_object=plate \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 # Object Partially Assembled, End-Effector Grasped (Near Goal) python scripts_v2/tools/record_reset_states.py \ --task OmniReset-UR5eRobotiq2f85-ObjectPartiallyAssembledEEGrasped-v0 \ - --num_envs 4096 --num_reset_states 10000 --headless \ + --num_envs 4096 --num_reset_states 10000 --visualizer none \ env.scene.insertive_object=cupcake env.scene.receptive_object=plate \ - env.events.reset_insertive_object_pose_from_partial_assembly_dataset.params.dataset_dir=./Datasets/OmniReset \ - env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset + env.events.reset_insertive_object_pose_from_partial_assembly_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 \ + env.events.reset_end_effector_pose_from_grasp_dataset.params.dataset_dir=./Datasets/OmniReset_isaaclab3 **Step 3.5: Visualize Reset States (Optional)** @@ -853,7 +878,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ env.scene.insertive_object=cupcake env.scene.receptive_object=plate .. tab-item:: Reaching @@ -862,7 +887,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectAnywhereEEAnywhere \ env.scene.insertive_object=cupcake env.scene.receptive_object=plate @@ -872,7 +897,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectRestingEEGrasped \ env.scene.insertive_object=cupcake env.scene.receptive_object=plate @@ -882,7 +907,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectAnywhereEEGrasped \ env.scene.insertive_object=cupcake env.scene.receptive_object=plate @@ -892,7 +917,7 @@ Reproduce our training results from scratch. python scripts_v2/tools/visualize_reset_states.py \ --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 \ - --num_envs 4 --dataset_dir ./Datasets/OmniReset \ + --num_envs 4 --dataset_dir ./Datasets/OmniReset_isaaclab3 \ --reset_type ObjectPartiallyAssembledEEGrasped \ env.scene.insertive_object=cupcake env.scene.receptive_object=plate @@ -909,7 +934,7 @@ Reproduce our training results from scratch. --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ env.scene.insertive_object=cupcake \ env.scene.receptive_object=plate @@ -925,11 +950,11 @@ Reproduce our training results from scratch. --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ env.scene.insertive_object=cupcake \ env.scene.receptive_object=plate \ - env.events.reset_from_reset_states.params.dataset_dir=./Datasets/OmniReset + env.events.reset_from_reset_states.params.dataset_dir=./Datasets/OmniReset_isaaclab3 **Training Curves** @@ -939,11 +964,11 @@ Reproduce our training results from scratch. * - .. figure:: ../../../source/_static/publications/omnireset/cupcake_success_rate_seeds.jpg :width: 100% - :alt: Cupcake on plate success rate over steps + :alt: Cupcake on plate success rate over PPO updates - .. figure:: ../../../source/_static/publications/omnireset/cupcake_success_rate_seeds_walltime.jpg :width: 100% - :alt: Cupcake on plate success rate over wall clock time + :alt: Cupcake on plate success rate over cumulative logged runtime ---- diff --git a/docs/source/publications/omnireset/sim2real.rst b/docs/source/publications/omnireset/sim2real.rst index 9f2c7c0b..dccf2c03 100644 --- a/docs/source/publications/omnireset/sim2real.rst +++ b/docs/source/publications/omnireset/sim2real.rst @@ -1,6 +1,11 @@ Sim2Real: SysID & RL Finetuning ================================ +.. note:: + + This workflow is expected to work with Isaac Lab 3.0 but has not yet been tested. + Validation is planned; use UWLab ``v1.3.0`` as the legacy reference. + This guide bridges sim-to-real via system identification and policy finetuning. Finetuning uses a curriculum: sim dynamics shift toward your sys-id'd parameters (with higher OSC gains to compensate for friction, since policies do not train well under high friction from scratch), and action scale is reduced so the policy runs slower and transfers better to the real robot. Our system identification follows the `PACE `_ framework by Bjelonic et al. @@ -12,15 +17,15 @@ Our system identification follows the `PACE `_ Pipeline overview ----------------- -1. **Robot setup** — UR5e/UR7e hardware config, robot calibration & USD, FK verification, metadata. Re-run reset state collection and RL training from :doc:`rl_training` (geometry-dependent). Install the diffusion_policy repo for real-robot control and sysid data collection. +1. **Robot setup**: UR5e/UR7e hardware config, robot calibration & USD, FK verification, metadata. Re-run reset state collection and RL training from :doc:`rl_training` (geometry-dependent). Install the diffusion_policy repo for real-robot control and sysid data collection. -2. **System identification** — Collect chirp on real robot, run CMA-ES in UWLab, verify fit, write sysid params to metadata, teleop to verify. +2. **System identification**: Collect chirp on real robot, run CMA-ES in UWLab, verify fit, write sysid params to metadata, teleop to verify. -3. **Finetune** — Select best Stage-1 checkpoint, finetune with ADR, evaluate. Or use our pre-finetuned checkpoints (next section) if your setup matches ours. +3. **Finetune**: Select best Stage-1 checkpoint, finetune with ADR, evaluate. -4. **Camera & hardware setup** — Mount cameras (D415/D435/D455), print task objects, calibrate camera extrinsics. +4. **Camera & hardware setup**: Mount cameras (D415/D435/D455), print task objects, calibrate camera extrinsics. -5. **Next** — :doc:`distillation` for vision policy training and real-robot deployment. +5. **Next**: :doc:`distillation` for vision policy training and real-robot deployment. ---- @@ -84,7 +89,7 @@ Install ROS 2 and set up the UR robot driver following the `NVIDIA Isaac ROS Uni **2. Update the robot USD** Download the existing calibrated robot USD from -`here `__ +`here `__ and open it in Isaac Sim. Replace the UR5e/UR7e arm in the USD with the URDF of your newly calibrated UR5e/UR7e. After replacing the arm, relink the joint that attaches the gripper to the arm. This joint connection must be re-established in Isaac Sim for the gripper to remain properly attached. **3. Verify alignment** @@ -97,7 +102,7 @@ Collect (joint_pos, ee_pose) pairs from the simulator using IK-based workspace s conda activate env_uwlab cd /UWLab python scripts_v2/tools/sim2real/collect_fk_pairs.py \ - --num_samples 4 --output /tmp/fk_pairs.npz --headless + --num_samples 4 --output /tmp/fk_pairs.npz --visualizer none .. code:: bash @@ -118,7 +123,7 @@ Place the calibrated USD and a ``metadata.yaml`` side by side: ur5e_robotiq_gripper_d415_mount_safety_calibrated.usd metadata.yaml -Copy the base ``metadata.yaml`` from `here `__ and update the ``calibrated_joints`` (xyz/rpy) and ``link_inertials`` (masses/coms/inertias) sections with the values from your calibrated URDF. The ``sysid`` block will be filled in after :ref:`system identification ` below. +Copy the base ``metadata.yaml`` from `here `__ and update the ``calibrated_joints`` (xyz/rpy) and ``link_inertials`` (masses/coms/inertias) sections with the values from your calibrated URDF. The ``sysid`` block will be filled in after :ref:`system identification ` below. **5. Recollect reset states & retrain** @@ -158,7 +163,7 @@ Use CMA-ES to optimize simulator dynamics parameters (armature, friction, motor conda activate env_uwlab cd /UWLab - python scripts_v2/tools/sim2real/sysid_ur5e_osc.py --headless \ + python scripts_v2/tools/sim2real/sysid_ur5e_osc.py --visualizer none \ --num_envs 512 \ --real_data /tmp/sysid_data_real.pt \ --max_iter 200 @@ -169,7 +174,7 @@ Plot simulated vs. real joint trajectories using the best checkpoint: .. code:: bash - python scripts_v2/tools/sim2real/plot_sysid_fit.py --headless \ + python scripts_v2/tools/sim2real/plot_sysid_fit.py --visualizer none \ --checkpoint logs/sysid//checkpoint_0200.pt \ --real_data /tmp/sysid_data_real.pt @@ -181,7 +186,7 @@ Inspect the overlay plots. A good fit should show close tracking across all join **4. Save parameters** -Replace the ``sysid`` block in ``metadata.yaml`` (next to your robot USD) with the identified values for ``armature``, ``static_friction``, ``dynamic_ratio``, and ``viscous_friction``. These are loaded automatically during finetuning and evaluation. See the current calibrated robot's `metadata.yaml `_ for reference. +Replace the ``sysid`` block in ``metadata.yaml`` (next to your robot USD) with the identified values for ``armature``, ``static_friction``, ``dynamic_ratio``, and ``viscous_friction``. These are loaded automatically during finetuning and evaluation. See the current calibrated robot's `metadata.yaml `_ for reference. **5. Teleop to verify motion** @@ -200,7 +205,7 @@ To tune gains if the arm lags or stalls, add ``--osc_kp_pos`` and ``--osc_kp_rot Select Best Checkpoint & Finetune with ADR -------------------------------------------- -Either run the pipeline below or use our pre-finetuned checkpoints (next section) if your setup matches ours. Some policies transfer better than others. As an offline proxy, evaluate candidate checkpoints under action noise and pick the one with the highest success rate, then finetune it with `ADR (Automatic Domain Randomization) `__. Finetuning uses the identified sysid parameters as the center of a randomization range that ADR automatically expands, producing a policy robust to real-world variation. +Some policies transfer better than others. As an offline proxy, evaluate candidate checkpoints under action noise and pick the one with the highest success rate, then finetune it with `ADR (Automatic Domain Randomization) `__. Finetuning uses the identified sysid parameters as the center of a randomization range that ADR automatically expands, producing a policy robust to real-world variation. ADR shifts the training distribution from zero friction, armature, and motor delay toward a randomization band around the sys-id'd values. OSC gains increase to compensate for higher friction. Action scale is reduced over the curriculum to slow the policy down for safer real-world transfer. @@ -227,7 +232,7 @@ All commands below run in the ``env_uwlab`` environment from the UWLab directory --action_noise 2.0 \ --eval_steps 1000 \ --num_envs 4096 \ - --headless \ + --visualizer none \ env.scene.insertive_object=peg \ env.scene.receptive_object=peghole @@ -239,7 +244,7 @@ All commands below run in the ``env_uwlab`` environment from the UWLab directory --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-v0 \ --num_envs 4096 \ --logger wandb \ - --headless \ + --visualizer none \ --resume_path \ env.scene.insertive_object=peg \ env.scene.receptive_object=peghole @@ -281,7 +286,7 @@ All commands below run in the ``env_uwlab`` environment from the UWLab directory --action_noise 2.0 \ --eval_steps 1000 \ --num_envs 4096 \ - --headless \ + --visualizer none \ env.scene.insertive_object=fbleg \ env.scene.receptive_object=fbtabletop @@ -296,7 +301,7 @@ All commands below run in the ``env_uwlab`` environment from the UWLab directory --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-v0 \ --num_envs 16384 \ --logger wandb \ - --headless \ + --visualizer none \ --distributed \ --resume_path \ env.scene.insertive_object=fbleg \ @@ -339,7 +344,7 @@ All commands below run in the ``env_uwlab`` environment from the UWLab directory --action_noise 2.0 \ --eval_steps 1000 \ --num_envs 4096 \ - --headless \ + --visualizer none \ env.scene.insertive_object=fbdrawerbottom \ env.scene.receptive_object=fbdrawerbox @@ -351,7 +356,7 @@ All commands below run in the ``env_uwlab`` environment from the UWLab directory --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-v0 \ --num_envs 8192 \ --logger wandb \ - --headless \ + --visualizer none \ --resume_path \ env.scene.insertive_object=fbdrawerbottom \ env.scene.receptive_object=fbdrawerbox @@ -383,148 +388,6 @@ All commands below run in the ``env_uwlab`` environment from the UWLab directory ---- -.. _use-finetuned-checkpoints: - -Use our finetuned checkpoints ------------------------------ - -Pre-finetuned for our robot calibration and sys-id'd parameters. If your setup is similar, you can download and run these instead of finetuning yourself. - -All commands below run in ``env_uwlab`` from the UWLab directory. - -.. tab-set:: - - .. tab-item:: Peg Insertion - - .. tab-set:: - - .. tab-item:: Seed 42 - - .. code:: bash - - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts_finetuned/peg_state_rl_expert_finetuned_seed42.pt - - python scripts/reinforcement_learning/rsl_rl/play.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-Play-v0 \ - --num_envs 1 \ - --checkpoint peg_state_rl_expert_finetuned_seed42.pt \ - env.scene.insertive_object=peg \ - env.scene.receptive_object=peghole - - .. tab-item:: Seed 0 - - .. code:: bash - - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts_finetuned/peg_state_rl_expert_finetuned_seed0.pt - - python scripts/reinforcement_learning/rsl_rl/play.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-Play-v0 \ - --num_envs 1 \ - --checkpoint peg_state_rl_expert_finetuned_seed0.pt \ - env.scene.insertive_object=peg \ - env.scene.receptive_object=peghole - - .. tab-item:: Seed 1 - - .. code:: bash - - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts_finetuned/peg_state_rl_expert_finetuned_seed1.pt - - python scripts/reinforcement_learning/rsl_rl/play.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-Play-v0 \ - --num_envs 1 \ - --checkpoint peg_state_rl_expert_finetuned_seed1.pt \ - env.scene.insertive_object=peg \ - env.scene.receptive_object=peghole - - .. tab-item:: Leg Twisting - - .. tab-set:: - - .. tab-item:: Seed 42 - - .. code:: bash - - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts_finetuned/leg_state_rl_expert_finetuned_seed42.pt - - python scripts/reinforcement_learning/rsl_rl/play.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-Play-v0 \ - --num_envs 1 \ - --checkpoint leg_state_rl_expert_finetuned_seed42.pt \ - env.scene.insertive_object=fbleg \ - env.scene.receptive_object=fbtabletop - - .. tab-item:: Seed 0 - - .. code:: bash - - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts_finetuned/leg_state_rl_expert_finetuned_seed0.pt - - python scripts/reinforcement_learning/rsl_rl/play.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-Play-v0 \ - --num_envs 1 \ - --checkpoint leg_state_rl_expert_finetuned_seed0.pt \ - env.scene.insertive_object=fbleg \ - env.scene.receptive_object=fbtabletop - - .. tab-item:: Seed 1 - - .. code:: bash - - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts_finetuned/leg_state_rl_expert_finetuned_seed1.pt - - python scripts/reinforcement_learning/rsl_rl/play.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-Play-v0 \ - --num_envs 1 \ - --checkpoint leg_state_rl_expert_finetuned_seed1.pt \ - env.scene.insertive_object=fbleg \ - env.scene.receptive_object=fbtabletop - - .. tab-item:: Drawer Assembly - - .. tab-set:: - - .. tab-item:: Seed 42 - - .. code:: bash - - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts_finetuned/drawer_state_rl_expert_finetuned_seed42.pt - - python scripts/reinforcement_learning/rsl_rl/play.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-Play-v0 \ - --num_envs 1 \ - --checkpoint drawer_state_rl_expert_finetuned_seed42.pt \ - env.scene.insertive_object=fbdrawerbottom \ - env.scene.receptive_object=fbdrawerbox - - .. tab-item:: Seed 0 - - .. code:: bash - - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts_finetuned/drawer_state_rl_expert_finetuned_seed0.pt - - python scripts/reinforcement_learning/rsl_rl/play.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-Play-v0 \ - --num_envs 1 \ - --checkpoint drawer_state_rl_expert_finetuned_seed0.pt \ - env.scene.insertive_object=fbdrawerbottom \ - env.scene.receptive_object=fbdrawerbox - - .. tab-item:: Seed 1 - - .. code:: bash - - wget https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Policies/OmniReset/state_based_experts_finetuned/drawer_state_rl_expert_finetuned_seed1.pt - - python scripts/reinforcement_learning/rsl_rl/play.py \ - --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Finetune-Play-v0 \ - --num_envs 1 \ - --checkpoint drawer_state_rl_expert_finetuned_seed1.pt \ - env.scene.insertive_object=fbdrawerbottom \ - env.scene.receptive_object=fbdrawerbox - ----- - .. _camera-hardware-setup: Camera & Hardware Setup @@ -532,8 +395,8 @@ Camera & Hardware Setup We use a **three-camera setup** with Intel RealSense depth cameras: -* **Wrist camera** — D415 mounted on the Robotiq 2F-85 gripper via a 3D-printed bracket. -* **Two third-person cameras** — D435 and D455 on tripods, providing front and side views. +* **Wrist camera**: D415 mounted on the Robotiq 2F-85 gripper via a 3D-printed bracket. +* **Two third-person cameras**: D435 and D455 on tripods, providing front and side views. Any combination of D415 / D435 / D455 works for any of the three viewpoints (the D455 has a wider baseline and higher depth quality, so prefer it when available). @@ -575,7 +438,7 @@ Virtual cameras in simulation must match your real camera poses and intrinsics s 1. The calibration workflow switches between two environments: ``robodiff_real`` for real-robot scripts (Step 1) and ``env_uwlab`` for UWLab simulation scripts (Step 2). Set up ``robodiff_real`` in :ref:`Installing Diffusion Policy ` above. -2. Print an ArUco marker — the calibration scripts use dictionary **6x6_50**, marker **ID 12**, printed at **150 mm**. Download the printable PDF: :download:`marker_6x6_150mm_id12.pdf <../../_static/publications/omnireset/marker_6x6_150mm_id12.pdf>`. +2. Print an ArUco marker: the calibration scripts use dictionary **6x6_50**, marker **ID 12**, printed at **150 mm**. Download the printable PDF: :download:`marker_6x6_150mm_id12.pdf <../../_static/publications/omnireset/marker_6x6_150mm_id12.pdf>`. 3. Place the printed marker flat on the table near the robot base (see the :ref:`camera setup photo ` above for an example placement). Measure the offset (in meters) from the marker center to the robot base-frame origin and update ``aruco_offset`` in ``0_camera_calibrate.py``. If you place the marker in the same position as our setup photo, the default ``[0.24, 0.0, 0.0]`` should work. @@ -595,7 +458,7 @@ Real-world scripts live in the `diffusion_policy /UWLab python scripts_v2/tools/sim2real/align_cameras.py \ - --enable_cameras \ - --headless \ + --visualizer none \ --camera front_camera \ --real_image /path/to/real_front.png \ --joint_angles @@ -633,7 +495,7 @@ Real-world scripts live in the `diffusion_policy /UWLab python scripts_v2/tools/sim2real/align_cameras.py \ - --enable_cameras \ - --headless \ + --visualizer none \ --camera side_camera \ --real_image /path/to/real_side.png \ --joint_angles @@ -671,7 +532,7 @@ Real-world scripts live in the `diffusion_policy /UWLab python scripts_v2/tools/sim2real/align_cameras.py \ - --enable_cameras \ - --headless \ + --visualizer none \ --camera wrist_camera \ --real_image /path/to/real_wrist.png \ --joint_angles @@ -717,6 +577,15 @@ After aligning each camera, paste the resulting ``pos``, ``rot``, and ``focal_le Update the ``TiledCameraCfg`` entries (``front_camera``, ``side_camera``, ``wrist_camera``) with the calibrated values. Also update the corresponding ``base_position`` and ``base_rotation`` in the randomization events (``randomize_front_camera``, ``randomize_side_camera``, ``randomize_wrist_camera``) to match. +.. caution:: + + Since the Isaac Lab 3.0 bump, every ``rot`` / ``base_rotation`` in these configs is a quaternion in + ``(x, y, z, w)`` order. ``align_cameras.py`` prints its ``rot`` in that order. ``2_get_isaacsim_extrinsics.py`` + in the diffusion_policy repo prints the 2.x ``(w, x, y, z)`` order, so move the first element to the end + before pasting its output (or convert with + ``isaaclab.utils.math.convert_quat(q, to="xyzw")``). A quaternion pasted in the old order is a + different camera pose, not an error. + With calibrated cameras, proceed to :doc:`distillation` to collect RGB demos, train a vision policy, evaluate in sim, and deploy on the real robot. ---- diff --git a/docs/source/refs/license.rst b/docs/source/refs/license.rst index bfbba6f8..ef93eb8e 100644 --- a/docs/source/refs/license.rst +++ b/docs/source/refs/license.rst @@ -5,7 +5,7 @@ License NVIDIA Isaac Sim is available freely under `individual license `_. For more information -about its license terms, please check `here `_. +about its license terms, consult the `official licensing information `_. The license files for all its dependencies and included assets are available in its `documentation `_. diff --git a/docs/source/setup/installation/binaries_installation.rst b/docs/source/setup/installation/binaries_installation.rst index 0dc833bf..97863066 100644 --- a/docs/source/setup/installation/binaries_installation.rst +++ b/docs/source/setup/installation/binaries_installation.rst @@ -13,8 +13,8 @@ Downloading pre-built binaries Isaac Sim binaries can be downloaded directly as a zip file from `here `__. -If you wish to use the older Isaac Sim 4.5 release, please check the older download page -`here `__. +Use Isaac Sim 6.1 with this release. For the older Isaac Lab 2.x / Isaac Sim 5.1 +stack, use UWLab's ``isaaclab2`` branch or ``v1.3.0`` tag and its installation instructions. Once the zip file is downloaded, you can unzip it to the desired directory. As an example set of instructions for unzipping the Isaac Sim binaries, diff --git a/docs/source/setup/installation/include/pip_python_virtual_env.rst b/docs/source/setup/installation/include/pip_python_virtual_env.rst index 4f6c87ec..45893b5f 100644 --- a/docs/source/setup/installation/include/pip_python_virtual_env.rst +++ b/docs/source/setup/installation/include/pip_python_virtual_env.rst @@ -20,13 +20,11 @@ You can choose different package managers to create a virtual environment. The Python version of the virtual environment must match the Python version of Isaac Sim. - - For Isaac Sim 5.X, the required Python version is 3.11. - - For Isaac Sim 4.X, the required Python version is 3.10. + - For Isaac Sim 6.X, the required Python version is 3.12. Using a different Python version will result in errors when running UW Lab. -The following instructions are for Isaac Sim 5.X, which requires Python 3.11. -If you wish to install Isaac Sim 4.5, please use modify the instructions accordingly to use Python 3.10. +The following instructions are for Isaac Sim 6.X, which requires Python 3.12. - Create a virtual environment using one of the package managers: @@ -45,8 +43,8 @@ If you wish to install Isaac Sim 4.5, please use modify the instructions accordi .. code-block:: bash - # create a virtual environment named env_uwlab with python3.11 - uv venv --python 3.11 env_uwlab + # create a virtual environment named env_uwlab with python3.12 + uv venv --python 3.12 env_uwlab # activate the virtual environment source env_uwlab/bin/activate @@ -55,8 +53,8 @@ If you wish to install Isaac Sim 4.5, please use modify the instructions accordi .. code-block:: batch - :: create a virtual environment named env_uwlab with python3.11 - uv venv --python 3.11 env_uwlab + :: create a virtual environment named env_uwlab with python3.12 + uv venv --python 3.12 env_uwlab :: activate the virtual environment env_uwlab\Scripts\activate @@ -70,7 +68,7 @@ If you wish to install Isaac Sim 4.5, please use modify the instructions accordi .. code-block:: bash - conda create -n env_uwlab python=3.11 + conda create -n env_uwlab python=3.12 conda activate env_uwlab .. tab-item:: venv Environment @@ -86,8 +84,8 @@ If you wish to install Isaac Sim 4.5, please use modify the instructions accordi .. code-block:: bash - # create a virtual environment named env_uwlab with python3.11 - python3.11 -m venv env_uwlab + # create a virtual environment named env_uwlab with python3.12 + python3.12 -m venv env_uwlab # activate the virtual environment source env_uwlab/bin/activate @@ -96,8 +94,8 @@ If you wish to install Isaac Sim 4.5, please use modify the instructions accordi .. code-block:: batch - :: create a virtual environment named env_uwlab with python3.11 - python3.11 -m venv env_uwlab + :: create a virtual environment named env_uwlab with python3.12 + python3.12 -m venv env_uwlab :: activate the virtual environment env_uwlab\Scripts\activate diff --git a/docs/source/setup/installation/include/src_clone_uwlab.rst b/docs/source/setup/installation/include/src_clone_uwlab.rst index 3a8b00df..f92af0a8 100644 --- a/docs/source/setup/installation/include/src_clone_uwlab.rst +++ b/docs/source/setup/installation/include/src_clone_uwlab.rst @@ -3,76 +3,36 @@ Cloning UW Lab .. note:: - We recommend making a `fork `_ of the UW Lab repository to contribute - to the project but this is not mandatory to use the framework. If you - make a fork, please replace ``isaac-sim`` with your username - in the following instructions. + We recommend making a `fork `_ to contribute, + but this is not required to use the framework. When using your fork, replace + ``uw-lab`` with your GitHub username in the clone commands. -Clone the UW Lab repository into your project's workspace: +Clone UW Lab into your project's workspace: .. tab-set:: .. tab-item:: SSH - .. code:: bash + .. code-block:: bash git clone git@github.com:uw-lab/UWLab.git .. tab-item:: HTTPS - .. code:: bash + .. code-block:: bash git clone https://github.com/uw-lab/UWLab.git +We provide the Linux helper `uwlab.sh `_ +to manage installation, Python execution, tests, and documentation. From the checkout, +print the supported commands with: -We provide a helper executable `uwlab.sh `_ -and `uwlab.bat `_ for Linux and Windows -respectively that provides utilities to manage extensions. +.. code-block:: bash -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: text - - ./uwlab.sh --help - - usage: uwlab.sh [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-n] [-c] -- Utility to manage UW Lab. - - optional arguments: - -h, --help Display the help content. - -i, --install [LIB] Install the extensions inside UW Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. - -f, --format Run pre-commit to format the code and check lints. - -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). - -s, --sim Run the simulator executable (isaac-sim.sh) provided by Isaac Sim. - -t, --test Run all python pytest tests. - -o, --docker Run the docker container helper script (docker/container.sh). - -v, --vscode Generate the VSCode settings file from template. - -d, --docs Build the documentation from source using sphinx. - -n, --new Create a new external project or internal task from template. - -c, --conda [NAME] Create the conda environment for UW Lab. Default name is 'env_uwlab'. - -u, --uv [NAME] Create the uv environment for UW Lab. Default name is 'env_uwlab'. - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: text - - uwlab.bat --help + ./uwlab.sh --help - usage: uwlab.bat [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-n] [-c] -- Utility to manage UW Lab. +.. warning:: - optional arguments: - -h, --help Display the help content. - -i, --install [LIB] Install the extensions inside UW Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. - -f, --format Run pre-commit to format the code and check lints. - -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). - -s, --sim Run the simulator executable (isaac-sim.bat) provided by Isaac Sim. - -t, --test Run all python pytest tests. - -v, --vscode Generate the VSCode settings file from template. - -d, --docs Build the documentation from source using sphinx. - -n, --new Create a new external project or internal task from template. - -c, --conda [NAME] Create the conda environment for UW Lab. Default name is 'env_uwlab'. - -u, --uv [NAME] Create the uv environment for UW Lab. Default name is 'env_uwlab'. + This repository does not ship ``uwlab.bat``. Do not assume that Isaac Lab's + Windows batch commands have UWLab equivalents; the helper instructions here + use the Linux shell entry point. diff --git a/docs/source/setup/installation/include/src_python_virtual_env.rst b/docs/source/setup/installation/include/src_python_virtual_env.rst index 2f594c57..5bf41c52 100644 --- a/docs/source/setup/installation/include/src_python_virtual_env.rst +++ b/docs/source/setup/installation/include/src_python_virtual_env.rst @@ -27,8 +27,7 @@ instead of *./uwlab.sh -p* or *uwlab.bat -p*. The Python version of the virtual environment must match the Python version of Isaac Sim. - - For Isaac Sim 5.X, the required Python version is 3.11. - - For Isaac Sim 4.X, the required Python version is 3.10. + - For Isaac Sim 6.X, the required Python version is 3.12. Using a different Python version will result in errors when running UW Lab. @@ -62,8 +61,8 @@ instead of *./uwlab.sh -p* or *uwlab.bat -p*. :sync: windows .. warning:: - Windows support for UV is currently unavailable. Please check - `issue #3483 `_ to track progress. + UWLab does not ship a Windows batch helper. Upstream Isaac Lab's Windows + UV support is discussed in `issue #3438 `_. .. tab-item:: Conda Environment diff --git a/docs/source/setup/installation/index.rst b/docs/source/setup/installation/index.rst index 93f3d9cb..008d61e3 100644 --- a/docs/source/setup/installation/index.rst +++ b/docs/source/setup/installation/index.rst @@ -3,13 +3,13 @@ Local Installation ================== -.. image:: https://img.shields.io/badge/IsaacSim-5.1.0-silver.svg +.. image:: https://img.shields.io/badge/IsaacSim-6.1.0-silver.svg :target: https://developer.nvidia.com/isaac-sim - :alt: IsaacSim 5.1.0 + :alt: IsaacSim 6.1.0 -.. image:: https://img.shields.io/badge/python-3.11-blue.svg - :target: https://www.python.org/downloads/release/python-31013/ - :alt: Python 3.11 +.. image:: https://img.shields.io/badge/python-3.12-blue.svg + :target: https://www.python.org/downloads/release/python-31211/ + :alt: Python 3.12 .. image:: https://img.shields.io/badge/platform-linux--64-orange.svg :target: https://releases.ubuntu.com/22.04/ @@ -26,8 +26,8 @@ recommended installation methods for both Isaac Sim and UW Lab. .. caution:: - We have dropped support for Isaac Sim versions 4.2.0 and below. We recommend using the latest - Isaac Sim 5.1.0 release to benefit from the latest features and improvements. + UW Lab requires Isaac Lab 3.0, which requires Isaac Sim 6.X. Isaac Sim 5.X and below are + not supported. For more information, please refer to the `Isaac Sim release notes `__. @@ -51,8 +51,7 @@ The basic requirements are: it essential to use the same Python version when installing UW Lab. The required Python version is as follows: -- For Isaac Sim 5.X, the required Python version is 3.11. -- For Isaac Sim 4.X, the required Python version is 3.10. +- For Isaac Sim 6.X, the required Python version is 3.12. Driver Requirements @@ -77,11 +76,12 @@ The DGX spark is a standalone machine learning device with aarch64 architecture. features of UW Lab are not currently supported on the DGX spark. The most noteworthy is that the architecture *requires* CUDA ≥ 13, and thus the cu13 build of PyTorch or newer. Other notable limitations with respect to UW Lab include... -#. `SkillGen `_ is not supported out of the box. This +#. `SkillGen `_ is not supported out of the box. This is because cuRobo builds native CUDA/C++ extensions that requires specific tooling and library versions which are not validated for use with DGX spark. -#. Extended reality teleoperation tools such as `OpenXR `_ is not supported. This is due - to encoding performance limitations that have not yet been fully investigated. +#. Extended reality teleoperation tools such as OpenXR are not supported. See the + `upstream platform limitations `_ + for the currently documented restrictions. #. SKRL training with `JAX `_ has not been explicitly validated or tested in UW Lab on the DGX Spark. JAX provides pre-built CUDA wheels only for Linux on x86_64, so on aarch64 systems (e.g., DGX Spark) it runs on CPU only by default. diff --git a/docs/source/setup/installation/pip_installation.rst b/docs/source/setup/installation/pip_installation.rst index 895717e2..9340c8c9 100644 --- a/docs/source/setup/installation/pip_installation.rst +++ b/docs/source/setup/installation/pip_installation.rst @@ -46,7 +46,7 @@ Installing dependencies .. code-block:: none - pip install "isaacsim[all,extscache]==5.1.0" --extra-index-url https://pypi.nvidia.com + pip install "isaacsim[all,extscache]==6.1.0.0" --extra-index-url https://pypi.nvidia.com - Install a CUDA-enabled PyTorch build that matches your system architecture: @@ -58,21 +58,21 @@ Installing dependencies .. code-block:: bash - pip install -U torch==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu128 + pip install -U torch==2.11.0 torchvision==0.26.0 --index-url https://download.pytorch.org/whl/cu128 .. tab-item:: :icon:`fa-brands fa-windows` Windows (x86_64) :sync: windows-x86_64 .. code-block:: bash - pip install -U torch==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu128 + pip install -U torch==2.11.0 torchvision==0.26.0 --index-url https://download.pytorch.org/whl/cu128 .. tab-item:: :icon:`fa-brands fa-linux` Linux (aarch64) :sync: linux-aarch64 .. code-block:: bash - pip install -U torch==2.9.0 torchvision==0.24.0 --index-url https://download.pytorch.org/whl/cu130 + pip install -U torch==2.11.0 torchvision==0.26.0 --index-url https://download.pytorch.org/whl/cu130 .. note:: diff --git a/environment.yml b/environment.yml index e4cd1de8..8b5e253a 100644 --- a/environment.yml +++ b/environment.yml @@ -7,7 +7,7 @@ channels: - conda-forge - defaults dependencies: - - python=3.11 + - python=3.12 - importlib_metadata - pip: - huggingface_hub diff --git a/scripts/environments/export_IODescriptors.py b/scripts/environments/export_IODescriptors.py index ba573365..ac39aa7e 100644 --- a/scripts/environments/export_IODescriptors.py +++ b/scripts/environments/export_IODescriptors.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -8,6 +8,7 @@ """Launch Isaac Sim Simulator first.""" import argparse +import contextlib import os from isaaclab.app import AppLauncher @@ -18,8 +19,8 @@ parser.add_argument("--output_dir", type=str, default=None, help="Path to the output directory.") # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) -# parse the arguments -args_cli = parser.parse_args() +# parse the arguments, forwarding unrecognized ones as Hydra-style task config overrides +args_cli, hydra_overrides = parser.parse_known_args() args_cli.headless = True # launch omniverse app @@ -33,6 +34,9 @@ import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 + +with contextlib.suppress(ImportError): + import isaaclab_tasks_experimental # noqa: F401 from isaaclab_tasks.utils import parse_env_cfg # PLACEHOLDER: Extension template (do not remove this comment) @@ -41,7 +45,9 @@ def main(): """Random actions agent with Isaac Lab environment.""" # create environment configuration - env_cfg = parse_env_cfg(args_cli.task, device=args_cli.device, num_envs=1, use_fabric=True) + env_cfg = parse_env_cfg( + args_cli.task, device=args_cli.device, num_envs=1, use_fabric=True, overrides=hydra_overrides + ) # create environment env = gym.make(args_cli.task, cfg=env_cfg) diff --git a/scripts/environments/list_envs.py b/scripts/environments/list_envs.py index 5c074388..5fd1b13e 100644 --- a/scripts/environments/list_envs.py +++ b/scripts/environments/list_envs.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,20 +9,28 @@ The script iterates over all registered environments and stores the details in a table. It prints the name of the environment, the entry point and the config file. -All the environments are registered in the `isaaclab_tasks` extension. They start -with `Isaac` in their name. +Environments are registered by the `isaaclab_tasks` and `uwlab_tasks` extensions. +Isaac Lab, UWLab, and OmniReset task names are included. """ -"""Launch Isaac Sim Simulator first.""" - -from isaaclab.app import AppLauncher - -# launch omniverse app -app_launcher = AppLauncher(headless=True) -simulation_app = app_launcher.app - - -"""Rest everything follows.""" +import argparse +import contextlib + +# add argparse arguments +parser = argparse.ArgumentParser(description="List Isaac Lab environments.") +parser.add_argument("--keyword", type=str, default=None, help="Keyword to filter environments.") +parser.add_argument( + "--show_presets", + action="store_true", + default=False, + help=( + "Show available preset selectors for each environment. " + "Presets are grouped by selector type: physics (physics=NAME), " + "renderer (renderer=NAME), and domain (presets=NAME)." + ), +) +# parse the arguments +args_cli = parser.parse_args() import gymnasium as gym from prettytable import PrettyTable @@ -30,36 +38,83 @@ import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 +# PLACEHOLDER: Extension template (do not remove this comment) +with contextlib.suppress(ImportError): + import isaaclab_tasks_experimental # noqa: F401 + + +def _format_presets(preset_map: dict | None) -> str: + """Format a preset map returned by :func:`enumerate_task_presets` into a human-readable string. + + Args: + preset_map: Mapping of :class:`~isaaclab_tasks.utils.preset_target.PresetTarget` + to sorted preset name lists, or ``None`` when the env cfg could not be loaded. + + Returns: + A multi-line string with one line per non-empty selector category, or a + short placeholder when no presets are available or the cfg failed to load. + """ + if preset_map is None: + return "(unavailable)" + from isaaclab_tasks.utils.preset_target import PresetTarget + + lines = [] + labels = { + PresetTarget.PHYSICS: "physics", + PresetTarget.RENDERER: "renderer", + PresetTarget.DOMAIN: "domain", + } + for target, label in labels.items(): + names = preset_map.get(target, []) + if names: + lines.append(f"{label}: {', '.join(names)}") + return "\n".join(lines) if lines else "(none)" + def main(): - """Print all environments registered in `isaaclab_tasks` extension.""" - # print all the available environments - table = PrettyTable(["S. No.", "Task Name", "Entry Point", "Config"]) - table.title = "Available Environments in Isaac Lab" - # set alignment of table columns - table.align["Task Name"] = "l" - table.align["Entry Point"] = "l" - table.align["Config"] = "l" - - # count of environments - index = 0 - # acquire all Isaac environments names - for task_spec in gym.registry.values(): - if "Isaac" in task_spec.id: - # add details to table - table.add_row([index + 1, task_spec.id, task_spec.entry_point, task_spec.kwargs["env_cfg_entry_point"]]) - # increment count - index += 1 + """Print registered Isaac Lab and UWLab environments.""" + # Collect matching task specs first so we can enumerate presets in one pass. + task_specs = [ + spec + for spec in gym.registry.values() + if ("Isaac" in spec.id or spec.id.startswith(("UW-", "OmniReset-"))) + and not spec.kwargs.get("deprecated") + and (args_cli.keyword is None or args_cli.keyword in spec.id) + ] + + if args_cli.show_presets: + from isaaclab_tasks.utils.preset_cli import enumerate_task_presets + + table = PrettyTable(["S. No.", "Task Name", "Entry Point", "Config", "Presets"]) + table.title = "Available Environments in Isaac Lab and UWLab" + table.align["Task Name"] = "l" + table.align["Entry Point"] = "l" + table.align["Config"] = "l" + table.align["Presets"] = "l" + + for index, spec in enumerate(task_specs): + preset_map = enumerate_task_presets(spec.id) + table.add_row( + [ + index + 1, + spec.id, + spec.entry_point, + spec.kwargs["env_cfg_entry_point"], + _format_presets(preset_map), + ] + ) + else: + table = PrettyTable(["S. No.", "Task Name", "Entry Point", "Config"]) + table.title = "Available Environments in Isaac Lab and UWLab" + table.align["Task Name"] = "l" + table.align["Entry Point"] = "l" + table.align["Config"] = "l" + + for index, spec in enumerate(task_specs): + table.add_row([index + 1, spec.id, spec.entry_point, spec.kwargs["env_cfg_entry_point"]]) print(table) if __name__ == "__main__": - try: - # run the main function - main() - except Exception as e: - raise e - finally: - # close the app - simulation_app.close() + main() diff --git a/scripts/environments/random_agent.py b/scripts/environments/random_agent.py index bec9817e..332b0f6f 100644 --- a/scripts/environments/random_agent.py +++ b/scripts/environments/random_agent.py @@ -1,73 +1,29 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -"""Script to an environment with random action agent.""" +"""Random-action agent executable for Isaac Lab environments.""" -"""Launch Isaac Sim Simulator first.""" - -import argparse - -from isaaclab.app import AppLauncher - -# add argparse arguments -parser = argparse.ArgumentParser(description="Random agent for Isaac Lab environments.") -parser.add_argument( - "--disable_fabric", action="store_true", default=False, help="Disable fabric and use USD I/O operations." -) -parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") -parser.add_argument("--task", type=str, default=None, help="Name of the task.") -# append AppLauncher cli args -AppLauncher.add_app_launcher_args(parser) -# parse the arguments -args_cli = parser.parse_args() +# PLACEHOLDER: Extension template (do not remove this comment) -# launch omniverse app -app_launcher = AppLauncher(args_cli) -simulation_app = app_launcher.app +# Warp captures ``enable_backward`` when a module is created, which happens at import +# time, so it has to be set before importing anything that defines Warp kernels. +# Isaac Lab does not use Warp autodiff; skipping adjoint codegen roughly halves the +# time spent building kernels on a cold kernel cache. +import warp as wp -"""Rest everything follows.""" +wp.config.enable_backward = False -import gymnasium as gym -import torch +from isaaclab_rl.entrypoints import run_random_agent_cli # noqa: E402 -import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -from isaaclab_tasks.utils import parse_env_cfg - -# PLACEHOLDER: Extension template (do not remove this comment) - - -def main(): - """Random actions agent with Isaac Lab environment.""" - # create environment configuration - env_cfg = parse_env_cfg( - args_cli.task, device=args_cli.device, num_envs=args_cli.num_envs, use_fabric=not args_cli.disable_fabric - ) - # create environment - env = gym.make(args_cli.task, cfg=env_cfg) - # print info (this is vectorized environment) - print(f"[INFO]: Gym observation space: {env.observation_space}") - print(f"[INFO]: Gym action space: {env.action_space}") - # reset environment - env.reset() - # simulate environment - while simulation_app.is_running(): - # run everything in inference mode - with torch.inference_mode(): - # sample actions from -1 to 1 - actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1 - # apply actions - env.step(actions) - # close the simulator - env.close() +def main(argv: list[str] | None = None) -> int: + """Run an environment with a random-action agent.""" + return run_random_agent_cli(argv) if __name__ == "__main__": - # run the main function - main() - # close sim app - simulation_app.close() + raise SystemExit(main()) diff --git a/scripts/environments/state_machine/lift_cube_sm.py b/scripts/environments/state_machine/lift_cube_sm.py index 4a2b6bca..dd1e9774 100644 --- a/scripts/environments/state_machine/lift_cube_sm.py +++ b/scripts/environments/state_machine/lift_cube_sm.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -11,7 +11,7 @@ .. code-block:: bash - ./isaaclab.sh -p scripts/environments/state_machine/lift_cube_sm.py --num_envs 32 + uv run python scripts/environments/state_machine/lift_cube_sm.py --num_envs 32 --viz kit """ @@ -29,26 +29,25 @@ parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) -# parse the arguments -args_cli = parser.parse_args() +# parse the arguments, forwarding unrecognized ones as Hydra-style task config overrides +args_cli, hydra_overrides = parser.parse_known_args() # launch omniverse app -app_launcher = AppLauncher(headless=args_cli.headless) +app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app """Rest everything else.""" -import gymnasium as gym -import torch from collections.abc import Sequence +import gymnasium as gym +import torch import warp as wp from isaaclab.assets.rigid_object.rigid_object_data import RigidObjectData import isaaclab_tasks # noqa: F401 -import uwlab_tasks # noqa: F401 -from isaaclab_tasks.manager_based.manipulation.lift.lift_env_cfg import LiftEnvCfg +from isaaclab_tasks.contrib.lift.lift_env_cfg import LiftEnvCfg from isaaclab_tasks.utils.parse_cfg import parse_env_cfg # initialize warp @@ -222,10 +221,6 @@ def reset_idx(self, env_ids: Sequence[int] = None): def compute(self, ee_pose: torch.Tensor, object_pose: torch.Tensor, des_object_pose: torch.Tensor) -> torch.Tensor: """Compute the desired state of the robot's end-effector and the gripper.""" - # convert all transformations from (w, x, y, z) to (x, y, z, w) - ee_pose = ee_pose[:, [0, 1, 2, 4, 5, 6, 3]] - object_pose = object_pose[:, [0, 1, 2, 4, 5, 6, 3]] - des_object_pose = des_object_pose[:, [0, 1, 2, 4, 5, 6, 3]] # convert to warp ee_pose_wp = wp.from_torch(ee_pose.contiguous(), wp.transform) @@ -251,22 +246,21 @@ def compute(self, ee_pose: torch.Tensor, object_pose: torch.Tensor, des_object_p device=self.device, ) - # convert transformations back to (w, x, y, z) - des_ee_pose = self.des_ee_pose[:, [0, 1, 2, 6, 3, 4, 5]] # convert to torch - return torch.cat([des_ee_pose, self.des_gripper_state.unsqueeze(-1)], dim=-1) + return torch.cat([self.des_ee_pose, self.des_gripper_state.unsqueeze(-1)], dim=-1) def main(): # parse configuration env_cfg: LiftEnvCfg = parse_env_cfg( - "Isaac-Lift-Cube-Franka-IK-Abs-v0", + "IsaacContrib-Lift-Cube-Franka-IK-Abs", device=args_cli.device, num_envs=args_cli.num_envs, use_fabric=not args_cli.disable_fabric, + overrides=hydra_overrides, ) # create environment - env = gym.make("Isaac-Lift-Cube-Franka-IK-Abs-v0", cfg=env_cfg) + env = gym.make("IsaacContrib-Lift-Cube-Franka-IK-Abs", cfg=env_cfg) # reset environment at start env.reset() @@ -290,11 +284,13 @@ def main(): # observations # -- end-effector frame ee_frame_sensor = env.unwrapped.scene["ee_frame"] - tcp_rest_position = ee_frame_sensor.data.target_pos_w[..., 0, :].clone() - env.unwrapped.scene.env_origins - tcp_rest_orientation = ee_frame_sensor.data.target_quat_w[..., 0, :].clone() + tcp_rest_position = ( + ee_frame_sensor.data.target_pos_w.torch[..., 0, :].clone() - env.unwrapped.scene.env_origins + ) + tcp_rest_orientation = ee_frame_sensor.data.target_quat_w.torch[..., 0, :].clone() # -- object frame object_data: RigidObjectData = env.unwrapped.scene["object"].data - object_position = object_data.root_pos_w - env.unwrapped.scene.env_origins + object_position = object_data.root_pos_w.torch - env.unwrapped.scene.env_origins # -- target object frame desired_position = env.unwrapped.command_manager.get_command("object_pose")[..., :3] diff --git a/scripts/environments/state_machine/lift_teddy_bear.py b/scripts/environments/state_machine/lift_franka_soft.py similarity index 56% rename from scripts/environments/state_machine/lift_teddy_bear.py rename to scripts/environments/state_machine/lift_franka_soft.py index e1a572fc..9e50bfca 100644 --- a/scripts/environments/state_machine/lift_teddy_bear.py +++ b/scripts/environments/state_machine/lift_franka_soft.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -11,47 +11,39 @@ .. code-block:: bash - ./isaaclab.sh -p scripts/environments/state_machine/lift_teddy_bear.py + # Kitless run with the Newton OpenGL viewer (default). + uv run python scripts/environments/state_machine/lift_franka_soft.py -""" + # Headless. + uv run python scripts/environments/state_machine/lift_franka_soft.py --viz none -"""Launch Omniverse Toolkit first.""" +""" import argparse - -from isaaclab.app import AppLauncher - -# add argparse arguments -parser = argparse.ArgumentParser(description="Pick and lift a teddy bear with a robotic arm.") -parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to simulate.") -# append AppLauncher cli args -AppLauncher.add_app_launcher_args(parser) -# parse the arguments -args_cli = parser.parse_args() - -# launch omniverse app -app_launcher = AppLauncher(headless=args_cli.headless) -simulation_app = app_launcher.app - -# disable metrics assembler due to scene graph instancing -from isaacsim.core.utils.extensions import disable_extension - -disable_extension("omni.usd.metrics.assembler.ui") - -"""Rest everything else.""" +import sys +from collections.abc import Sequence import gymnasium as gym import torch -from collections.abc import Sequence - import warp as wp -from isaaclab.assets.rigid_object.rigid_object_data import RigidObjectData +from isaaclab.app import add_launcher_args, launch_simulation +from isaaclab.assets.deformable_object.deformable_object_data import DeformableObjectData +from isaaclab.visualizers import VisualizerCfg import isaaclab_tasks # noqa: F401 -import uwlab_tasks # noqa: F401 -from isaaclab_tasks.manager_based.manipulation.lift.lift_env_cfg import LiftEnvCfg -from isaaclab_tasks.utils.parse_cfg import parse_env_cfg +from isaaclab_tasks.utils import resolve_task_config, setup_preset_cli + +# add argparse arguments +parser = argparse.ArgumentParser(description="Pick and lift a deformable with a robotic arm.") +parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to simulate.") +parser.add_argument("--num_steps", type=int, default=1000, help="Number of environment steps to run.") +parser.add_argument("--task", type=str, default="Isaac-Lift-Soft-Franka", help="The task to run.") +add_launcher_args(parser) +# the task runs on Newton, so default to the kitless viewer +parser.set_defaults(visualizer=["newton"]) +args_cli, hydra_args = setup_preset_cli(parser) +sys.argv = [sys.argv[0]] + hydra_args # initialize warp wp.init() @@ -75,17 +67,6 @@ class PickSmState: OPEN_GRIPPER = wp.constant(5) -class PickSmWaitTime: - """Additional wait times (in s) for states for before switching.""" - - REST = wp.constant(0.2) - APPROACH_ABOVE_OBJECT = wp.constant(0.5) - APPROACH_OBJECT = wp.constant(0.6) - GRASP_OBJECT = wp.constant(0.6) - LIFT_OBJECT = wp.constant(1.0) - OPEN_GRIPPER = wp.constant(0.0) - - @wp.func def distance_below_threshold(current_pos: wp.vec3, desired_pos: wp.vec3, threshold: float) -> bool: return wp.length(current_pos - desired_pos) < threshold @@ -176,6 +157,20 @@ def infer_state_machine( sm_wait_time[tid] = sm_wait_time[tid] + dt[tid] +class PickSmWaitTime: + """Additional wait times (in s) for states for before switching. + + Wait times are generous because the low-PD Franka takes a while to settle on each IK target. + """ + + REST = wp.constant(0.2) + APPROACH_ABOVE_OBJECT = wp.constant(1.0) + APPROACH_OBJECT = wp.constant(1.5) + GRASP_OBJECT = wp.constant(1.5) + LIFT_OBJECT = wp.constant(1.5) + OPEN_GRIPPER = wp.constant(0.0) + + class PickAndLiftSm: """A simple state machine in a robot's task space to pick and lift an object. @@ -191,7 +186,7 @@ class PickAndLiftSm: 5. LIFT_OBJECT: The robot lifts the object to the desired pose. This is the final state. """ - def __init__(self, dt: float, num_envs: int, device: torch.device | str = "cpu", position_threshold=0.01): + def __init__(self, dt: float, num_envs: int, device: torch.device | str = "cpu", position_threshold=0.03): """Initialize the state machine. Args: @@ -215,7 +210,7 @@ def __init__(self, dt: float, num_envs: int, device: torch.device | str = "cpu", # approach above object offset self.offset = torch.zeros((self.num_envs, 7), device=self.device) - self.offset[:, 2] = 0.2 + self.offset[:, 2] = 0.1 self.offset[:, -1] = 1.0 # warp expects quaternion as (x, y, z, w) # convert to warp @@ -235,10 +230,6 @@ def reset_idx(self, env_ids: Sequence[int] = None): def compute(self, ee_pose: torch.Tensor, object_pose: torch.Tensor, des_object_pose: torch.Tensor): """Compute the desired state of the robot's end-effector and the gripper.""" - # convert all transformations from (w, x, y, z) to (x, y, z, w) - ee_pose = ee_pose[:, [0, 1, 2, 4, 5, 6, 3]] - object_pose = object_pose[:, [0, 1, 2, 4, 5, 6, 3]] - des_object_pose = des_object_pose[:, [0, 1, 2, 4, 5, 6, 3]] # convert to warp ee_pose_wp = wp.from_torch(ee_pose.contiguous(), wp.transform) @@ -264,79 +255,103 @@ def compute(self, ee_pose: torch.Tensor, object_pose: torch.Tensor, des_object_p device=self.device, ) - # convert transformations back to (w, x, y, z) - des_ee_pose = self.des_ee_pose[:, [0, 1, 2, 6, 3, 4, 5]] # convert to torch - return torch.cat([des_ee_pose, self.des_gripper_state.unsqueeze(-1)], dim=-1) + return torch.cat([self.des_ee_pose, self.des_gripper_state.unsqueeze(-1)], dim=-1) def main(): - # parse configuration - env_cfg: LiftEnvCfg = parse_env_cfg( - "Isaac-Lift-Teddy-Bear-Franka-IK-Abs-v0", - device=args_cli.device, - num_envs=args_cli.num_envs, - ) - - env_cfg.viewer.eye = (2.1, 1.0, 1.3) - - # create environment - env = gym.make("Isaac-Lift-Teddy-Bear-Franka-IK-Abs-v0", cfg=env_cfg) - # reset environment at start - env.reset() - - # create action buffers (position + quaternion) - actions = torch.zeros(env.unwrapped.action_space.shape, device=env.unwrapped.device) - actions[:, 3] = 1.0 - # desired rotation after grasping - desired_orientation = torch.zeros((env.unwrapped.num_envs, 4), device=env.unwrapped.device) - desired_orientation[:, 1] = 1.0 - - object_grasp_orientation = torch.zeros((env.unwrapped.num_envs, 4), device=env.unwrapped.device) - # z-axis pointing down and 45 degrees rotation - object_grasp_orientation[:, 1] = 0.9238795 - object_grasp_orientation[:, 2] = -0.3826834 - object_local_grasp_position = torch.tensor([0.02, -0.08, 0.0], device=env.unwrapped.device) - - # create state machine - pick_sm = PickAndLiftSm(env_cfg.sim.dt * env_cfg.decimation, env.unwrapped.num_envs, env.unwrapped.device) - - while simulation_app.is_running(): - # run everything in inference mode - with torch.inference_mode(): - # step environment - dones = env.step(actions)[-2] - - # observations - # -- end-effector frame - ee_frame_sensor = env.unwrapped.scene["ee_frame"] - tcp_rest_position = ee_frame_sensor.data.target_pos_w[..., 0, :].clone() - env.unwrapped.scene.env_origins - tcp_rest_orientation = ee_frame_sensor.data.target_quat_w[..., 0, :].clone() - # -- object frame - object_data: RigidObjectData = env.unwrapped.scene["object"].data - object_position = object_data.root_pos_w - env.unwrapped.scene.env_origins - object_position += object_local_grasp_position - - # -- target object frame - desired_position = env.unwrapped.command_manager.get_command("object_pose")[..., :3] - - # advance state machine - actions = pick_sm.compute( - torch.cat([tcp_rest_position, tcp_rest_orientation], dim=-1), - torch.cat([object_position, object_grasp_orientation], dim=-1), - torch.cat([desired_position, desired_orientation], dim=-1), - ) - - # reset state machine - if dones.any(): - pick_sm.reset_idx(dones.nonzero(as_tuple=False).squeeze(-1)) - - # close the environment - env.close() + # parse configuration via Hydra, so presets can be selected on the CLI (e.g. presets=isaacsim_physx) + env_cfg, _ = resolve_task_config(args_cli.task, "") + env_cfg.sim.device = args_cli.device + env_cfg.scene.num_envs = args_cli.num_envs + # Scripted demo: keep only the time-out, extended to 10 s so the slow low-PD Franka can finish a + # pick-and-lift, and drop the failure terminations so a transient bound or velocity spike does + # not cut a run short. + env_cfg.episode_length_s = 10.0 + for term_name in list(vars(env_cfg.terminations)): + if term_name != "time_out": + setattr(env_cfg.terminations, term_name, None) + # the state machine emits absolute end-effector poses, so pick the task's own IK action preset + # (e.g. the cloth closes the gripper fully); the env otherwise defaults to relative joint + # targets, which RL trains on. + env_cfg.actions = type(env_cfg)().actions.ik + env_cfg.viewer.eye = (1.3, 0.6, 0.5) + env_cfg.viewer.lookat = (0.5, 0.0, 0.05) + env_cfg.sim.default_visualizer_cfg = VisualizerCfg(eye=env_cfg.viewer.eye, lookat=env_cfg.viewer.lookat) + + with launch_simulation(env_cfg, args_cli): + env = gym.make(args_cli.task, cfg=env_cfg) + is_cable = "cable" in env.unwrapped.scene.keys() + + # reset environment at start + env.reset() + + # create action buffers (position + quaternion) + actions = torch.zeros(env.unwrapped.action_space.shape, device=env.unwrapped.device) + actions[:, 3] = 1.0 + # desired rotation after grasping + desired_orientation = torch.zeros((env.unwrapped.num_envs, 4), device=env.unwrapped.device) + desired_orientation[:, 0] = 1.0 + + # Top-down approach: identity quaternion (wxyz, w=1) aligns panda_hand with the Franka root, + # giving the canonical top-down grasp pose. The bar lies along world-X, so the gripper + # closes across its short side without any wrist twist. + object_grasp_orientation = torch.zeros((env.unwrapped.num_envs, 4), device=env.unwrapped.device) + object_grasp_orientation[:, 0] = 1.0 + # Grasp 1 cm below the deformable's centre of mass, so the fingers close around its lower half. + # The cloth drapes over a support cube, so its COM sits well below the graspable fold; reach + # 8 cm higher to close on the raised cloth instead of the table. + grasp_z = -0.01 + (0.08 if "Cloth" in args_cli.task else 0.0) + object_local_grasp_position = torch.tensor([0.0, 0.0, grasp_z], device=env.unwrapped.device) + + # create state machine + pick_sm = PickAndLiftSm(env_cfg.sim.dt * env_cfg.decimation, env.unwrapped.num_envs, env.unwrapped.device) + + for _ in range(args_cli.num_steps): + # run everything in inference mode + with torch.inference_mode(): + # step environment + _, _, terminated, time_outs, _ = env.step(actions) + dones = terminated | time_outs + + # reset state machine + if dones.any(): + pick_sm.reset_idx(dones.nonzero(as_tuple=False).squeeze(-1)) + + # observations + # -- end-effector frame + ee_frame_sensor = env.unwrapped.scene["ee_frame"] + tcp_rest_position = ( + ee_frame_sensor.data.target_pos_w.torch[..., 0, :].clone() - env.unwrapped.scene.env_origins + ) + tcp_rest_orientation = ee_frame_sensor.data.target_quat_w.torch[..., 0, :].clone() + # -- object frame + if is_cable: + segment_index = env_cfg.commands.cable_pose.segment_index + object_position = ( + env.unwrapped.scene["cable"].data.segment_pose_w.torch[:, segment_index, :3] + - env.unwrapped.scene.env_origins + ) + command_name = "cable_pose" + else: + object_data: DeformableObjectData = env.unwrapped.scene["deformable"].data + object_position = object_data.root_pos_w.torch - env.unwrapped.scene.env_origins + object_position += object_local_grasp_position + command_name = "deformable_pose" + + # -- target object frame + desired_position = env.unwrapped.command_manager.get_command(command_name)[..., :3] + + # advance state machine + actions = pick_sm.compute( + torch.cat([tcp_rest_position, tcp_rest_orientation], dim=-1), + torch.cat([object_position, object_grasp_orientation], dim=-1), + torch.cat([desired_position, desired_orientation], dim=-1), + ) + + # close the environment + env.close() if __name__ == "__main__": - # run the main function main() - # close sim app - simulation_app.close() diff --git a/scripts/environments/state_machine/open_cabinet_sm.py b/scripts/environments/state_machine/open_cabinet_sm.py index 9ddeb0fd..68710111 100644 --- a/scripts/environments/state_machine/open_cabinet_sm.py +++ b/scripts/environments/state_machine/open_cabinet_sm.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -11,7 +11,7 @@ .. code-block:: bash - ./isaaclab.sh -p scripts/environments/state_machine/open_cabinet_sm.py --num_envs 32 + uv run python scripts/environments/state_machine/open_cabinet_sm.py --num_envs 32 --viz kit """ @@ -29,26 +29,25 @@ parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) -# parse the arguments -args_cli = parser.parse_args() +# parse the arguments, forwarding unrecognized ones as Hydra-style task config overrides +args_cli, hydra_overrides = parser.parse_known_args() # launch omniverse app -app_launcher = AppLauncher(headless=args_cli.headless) +app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app """Rest everything else.""" -import gymnasium as gym -import torch from collections.abc import Sequence +import gymnasium as gym +import torch import warp as wp from isaaclab.sensors import FrameTransformer import isaaclab_tasks # noqa: F401 -import uwlab_tasks # noqa: F401 -from isaaclab_tasks.manager_based.manipulation.cabinet.cabinet_env_cfg import CabinetEnvCfg +from isaaclab_tasks.core.cabinet.cabinet_env_cfg import CabinetEnvCfg from isaaclab_tasks.utils.parse_cfg import parse_env_cfg # initialize warp @@ -242,9 +241,6 @@ def reset_idx(self, env_ids: Sequence[int] | None = None): def compute(self, ee_pose: torch.Tensor, handle_pose: torch.Tensor): """Compute the desired state of the robot's end-effector and the gripper.""" - # convert all transformations from (w, x, y, z) to (x, y, z, w) - ee_pose = ee_pose[:, [0, 1, 2, 4, 5, 6, 3]] - handle_pose = handle_pose[:, [0, 1, 2, 4, 5, 6, 3]] # convert to warp ee_pose_wp = wp.from_torch(ee_pose.contiguous(), wp.transform) handle_pose_wp = wp.from_torch(handle_pose.contiguous(), wp.transform) @@ -269,22 +265,21 @@ def compute(self, ee_pose: torch.Tensor, handle_pose: torch.Tensor): device=self.device, ) - # convert transformations back to (w, x, y, z) - des_ee_pose = self.des_ee_pose[:, [0, 1, 2, 6, 3, 4, 5]] # convert to torch - return torch.cat([des_ee_pose, self.des_gripper_state.unsqueeze(-1)], dim=-1) + return torch.cat([self.des_ee_pose, self.des_gripper_state.unsqueeze(-1)], dim=-1) def main(): # parse configuration env_cfg: CabinetEnvCfg = parse_env_cfg( - "Isaac-Open-Drawer-Franka-IK-Abs-v0", + "IsaacContrib-Open-Drawer-Franka-IK-Abs", device=args_cli.device, num_envs=args_cli.num_envs, use_fabric=not args_cli.disable_fabric, + overrides=hydra_overrides, ) # create environment - env = gym.make("Isaac-Open-Drawer-Franka-IK-Abs-v0", cfg=env_cfg) + env = gym.make("IsaacContrib-Open-Drawer-Franka-IK-Abs", cfg=env_cfg) # reset environment at start env.reset() @@ -306,12 +301,14 @@ def main(): # observations # -- end-effector frame ee_frame_tf: FrameTransformer = env.unwrapped.scene["ee_frame"] - tcp_rest_position = ee_frame_tf.data.target_pos_w[..., 0, :].clone() - env.unwrapped.scene.env_origins - tcp_rest_orientation = ee_frame_tf.data.target_quat_w[..., 0, :].clone() + tcp_rest_position = ee_frame_tf.data.target_pos_w.torch[..., 0, :].clone() - env.unwrapped.scene.env_origins + tcp_rest_orientation = ee_frame_tf.data.target_quat_w.torch[..., 0, :].clone() # -- handle frame cabinet_frame_tf: FrameTransformer = env.unwrapped.scene["cabinet_frame"] - cabinet_position = cabinet_frame_tf.data.target_pos_w[..., 0, :].clone() - env.unwrapped.scene.env_origins - cabinet_orientation = cabinet_frame_tf.data.target_quat_w[..., 0, :].clone() + cabinet_position = ( + cabinet_frame_tf.data.target_pos_w.torch[..., 0, :].clone() - env.unwrapped.scene.env_origins + ) + cabinet_orientation = cabinet_frame_tf.data.target_quat_w.torch[..., 0, :].clone() # advance state machine actions = open_sm.compute( diff --git a/scripts/environments/teleoperation/teleop_se3_agent.py b/scripts/environments/teleoperation/teleop_se3_agent.py index 43dfeb25..24532be4 100644 --- a/scripts/environments/teleoperation/teleop_se3_agent.py +++ b/scripts/environments/teleoperation/teleop_se3_agent.py @@ -1,85 +1,256 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -"""Script to run a keyboard teleoperation with Isaac Lab manipulation environments.""" +"""Script to run teleoperation with Isaac Lab manipulation environments. + +Supports multiple input devices (e.g., keyboard, spacemouse, gamepad) and devices +configured within the environment (including OpenXR-based hand tracking or motion +controllers). + +This script supports two teleoperation stacks: +1. Native Isaac Lab teleop stack (via teleop_devices in env_cfg) +2. IsaacTeleop-based stack (via isaac_teleop in env_cfg) + +The script automatically detects which stack to use based on the environment config. +""" """Launch Isaac Sim Simulator first.""" +# Isaac Lab does not use Warp autodiff; skipping adjoint codegen roughly halves the +# time spent building kernels on a cold kernel cache. +import warp as wp + +wp.config.enable_backward = False + import argparse +import sys from collections.abc import Callable from isaaclab.app import AppLauncher +from isaaclab.utils.string import list_intersection, string_to_callable + +from isaaclab_tasks.utils import setup_preset_cli # add argparse arguments -parser = argparse.ArgumentParser(description="Keyboard teleoperation for Isaac Lab environments.") +parser = argparse.ArgumentParser(description="Teleoperation for Isaac Lab environments.") parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to simulate.") parser.add_argument( "--teleop_device", type=str, - default="keyboard", + default=None, help=( - "Teleop device. Set here (legacy) or via the environment config. If using the environment config, pass the" - " device key/name defined under 'teleop_devices' (it can be a custom name, not necessarily 'handtracking')." - " Built-ins: keyboard, spacemouse, gamepad. Not all tasks support all built-ins." + "Legacy teleop device name. When omitted, the IsaacTeleop pipeline is used if configured in the env," + " otherwise keyboard is used as fallback. When explicitly provided, the script uses the legacy" + " teleop_devices path and looks up this name in env_cfg.teleop_devices.devices." ), ) parser.add_argument("--task", type=str, default=None, help="Name of the task.") parser.add_argument("--sensitivity", type=float, default=1.0, help="Sensitivity factor.") parser.add_argument( - "--enable_pinocchio", + "--cloudxr_env", + type=str, + default=None, + help=( + "Path to a CloudXR .env file, or a shorthand: 'cloudxrjs' (Quest/Pico), 'avp' (Apple Vision Pro)," + " or 'standalone' (headless, no XR client). Set to 'none' to disable CloudXR auto-launch entirely." + " When unset, defaults to 'cloudxrjs' with --xr and 'standalone' without --xr." + ), +) +parser.add_argument( + "--auto_launch_cloudxr", + action=argparse.BooleanOptionalAction, + default=True, + help="Auto-launch the CloudXR runtime when --cloudxr_env is set. Use --no-auto_launch_cloudxr to disable.", +) +parser.add_argument( + "--enable_debug_visualization", action="store_true", default=False, - help="Enable Pinocchio.", + help="Enable hand joint and controller aim debug visualization at session start (IsaacTeleop only).", +) +parser.add_argument( + "--external_callback", + default=None, + help="Fully qualified path to an externally defined callback.", ) +parser.add_argument( + "--disable_external_cameras", + action="store_true", + default=False, + help=( + "Disable external camera rendering. External cameras render by default for teleoperation;" + " pass this flag to strip camera sensors from the environment (e.g. to reduce GPU contention" + " and improve XR performance)." + ), +) + # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments -args_cli = parser.parse_args() +args_cli, hydra_args = setup_preset_cli(parser) app_launcher_args = vars(args_cli) -if args_cli.enable_pinocchio: - # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and - # not the one installed by Isaac Sim pinocchio is required by the Pink IK controllers and the - # GR1T2 retargeter - import pinocchio # noqa: F401 -if "handtracking" in args_cli.teleop_device.lower(): - app_launcher_args["xr"] = True - -# launch omniverse app -app_launcher = AppLauncher(app_launcher_args) +# Enable external camera rendering by default (``--disable_external_cameras`` turns it off). The +# ``--enable_cameras`` CLI flag was removed in Isaac Lab 3.0 (see #6656), so pass the intent to +# AppLauncher as a kwarg; this selects a camera-rendering experience that provides RTX/DLSS support. +# Everywhere else we read ``args_cli.disable_external_cameras`` directly. +app_launcher = AppLauncher(app_launcher_args, enable_cameras=not args_cli.disable_external_cameras) simulation_app = app_launcher.app +# Call an external callback if requested. +remaining_args_env_registration = None +if args_cli.external_callback: + external_callback_function = string_to_callable(args_cli.external_callback, separator=".") + remaining_args_env_registration = external_callback_function() + +# Hand arguments consumed by neither this parser nor the callback over to Hydra. +hydra_args = list_intersection(hydra_args, remaining_args_env_registration) +sys.argv = [sys.argv[0]] + hydra_args + """Rest everything follows.""" -import gymnasium as gym import logging + +import gymnasium as gym import torch +from isaaclab_physx.renderers import IsaacRtxRendererGlobalSettingsCfg +from isaaclab_physx.renderers.isaac_rtx_renderer_utils import ( + apply_isaac_rtx_global_settings, +) from isaaclab.devices import Se3Gamepad, Se3GamepadCfg, Se3Keyboard, Se3KeyboardCfg, Se3SpaceMouse, Se3SpaceMouseCfg from isaaclab.devices.openxr import remove_camera_configs from isaaclab.devices.teleop_device_factory import create_teleop_device +from isaaclab.envs import ManagerBasedRLEnvCfg from isaaclab.managers import TerminationTermCfg as DoneTerm import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -from isaaclab_tasks.manager_based.manipulation.lift import mdp -from isaaclab_tasks.utils import parse_env_cfg - -if args_cli.enable_pinocchio: - import isaaclab_tasks.manager_based.locomanipulation.pick_place # noqa: F401 - import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 +from isaaclab_tasks.core.lift import mdp +from isaaclab_tasks.utils import resolve_task_config -# import logger logger = logging.getLogger(__name__) +_CLOUDXR_ENV_SHORTHANDS: dict[str, str] = {} + -def main() -> None: +def _resolve_cloudxr_env(value: str | None, xr_enabled: bool = False) -> str | None: + """Resolve ``--cloudxr_env`` shorthands to absolute ``.env`` file paths. + + Accepts ``"cloudxrjs"`` (Quest/Pico), ``"avp"`` (Apple Vision Pro), + ``"standalone"`` (headless, no XR client), ``"none"`` (disable), or an + arbitrary file path. When *value* is ``None`` (flag unset), defaults to + ``"cloudxrjs"`` when *xr_enabled* else ``"standalone"`` -- so a run without + ``--xr`` uses the clientless headless profile. """ - Run keyboard teleoperation with Isaac Lab manipulation environment. + if value is None: + value = "cloudxrjs" if xr_enabled else "standalone" + if value.strip() == "" or value.lower() == "none": + return None + if not _CLOUDXR_ENV_SHORTHANDS: + from isaaclab_teleop import CLOUDXR_AVP_ENV, CLOUDXR_JS_ENV, CLOUDXR_STANDALONE_ENV + + _CLOUDXR_ENV_SHORTHANDS["cloudxrjs"] = CLOUDXR_JS_ENV + _CLOUDXR_ENV_SHORTHANDS["avp"] = CLOUDXR_AVP_ENV + _CLOUDXR_ENV_SHORTHANDS["standalone"] = CLOUDXR_STANDALONE_ENV + return _CLOUDXR_ENV_SHORTHANDS.get(value.lower(), value) + + +def _rtx_rendering_requested(args: argparse.Namespace) -> bool: + """Return whether the CLI selects a renderer that actually drives RTX rendering. + + The RTX/DLSS global settings are only meaningful when something renders through RTX. + That happens when the Kit visualizer is enabled (``--viz kit``), when external cameras + are rendered (on by default; see ``--disable_external_cameras``), or in XR mode (``--xr``). + A pure-headless session with none of these renders nothing. + + This reads the resolved namespace intent rather than any Kit/carb runtime state so the + check keeps working as these scripts grow support for other renderers and kitless runs. + """ + visualizers = getattr(args, "visualizer", None) or [] + external_cameras = not getattr(args, "disable_external_cameras", False) + return external_cameras or ("kit" in visualizers) or bool(getattr(args, "xr", False)) + + +def _ensure_replicator_loaded() -> None: + """Enable ``omni.replicator.core`` so RTX/DLSS global settings can be applied. + + :func:`apply_isaac_rtx_global_settings` sets the antialiasing mode through + ``omni.replicator.core``, which ships with the SDG/rendering extensions. Some Kit + experiences (e.g. the Kit-viewport-only app selected by ``--visualizer kit`` without + cameras or XR) do not preload it, so enable it on demand via the extension manager + before applying RTX settings. Idempotent when the extension is already enabled. + """ + import omni.kit.app + + omni.kit.app.get_app().get_extension_manager().set_extension_enabled_immediate("omni.replicator.core", True) + + +def _create_builtin_device(device_name: str, sensitivity: float) -> object | None: + """Create a built-in teleop device by name, or return None if unrecognized.""" + name = device_name.lower() + if name == "keyboard": + return Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.05 * sensitivity, rot_sensitivity=0.05 * sensitivity)) + elif name == "spacemouse": + return Se3SpaceMouse(Se3SpaceMouseCfg(pos_sensitivity=0.05 * sensitivity, rot_sensitivity=0.05 * sensitivity)) + elif name == "gamepad": + return Se3Gamepad(Se3GamepadCfg(pos_sensitivity=0.1 * sensitivity, rot_sensitivity=0.1 * sensitivity)) + return None + + +def _make_haptic_io(env, teleop_interface, env_cfg, use_isaac_teleop: bool): + """Return ``(update, stop)`` callables driving controller haptics, or no-ops. + + Keeps haptics opt-in without branching in the main loop: both callables are + no-ops unless the active device is an IsaacTeleop device and the env declares + a ``haptic_feedback`` config. ``update`` renders the current contact force; + ``stop`` zeroes it so a stale pulse does not persist while teleop is paused. + """ + noop = lambda: None # noqa: E731 + if not use_isaac_teleop: + return noop, noop + from isaaclab_teleop import create_haptic_feedback_driver + + driver = create_haptic_feedback_driver(env.unwrapped, teleop_interface, env_cfg) + if driver is None: + return noop, noop + return driver.update, driver.stop + + +def _make_control_keyboard(teleop_interface, use_isaac_teleop: bool, has_window: bool): + """Create an optional keyboard for headset-free IsaacTeleop control. + + Binds ``B`` / ``P`` / ``R`` to start-resume / pause / reset so a user can drive + the teleop state machine without an XR headset. Keys are captured through the app + window, so this returns ``None`` when there is no window or when IsaacTeleop is + not the active stack (a windowless run still auto-starts teleop). ``R`` is an operator + reset: :meth:`~isaaclab_teleop.IsaacTeleopDevice.reset` with ``pause=True`` injects a + single RESET pulse (the loop's control-event handler turns it into one environment + reset) and pauses the session (binding it straight to the reset callback would reset + the env twice). The returned device must be kept referenced by the caller so its carb + input subscription survives. + """ + if not use_isaac_teleop or not has_window: + return None + try: + keyboard = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.0, rot_sensitivity=0.0)) + keyboard.add_callback("B", teleop_interface.request_start) + keyboard.add_callback("P", teleop_interface.request_stop) + keyboard.add_callback("R", lambda: teleop_interface.reset(pause=True)) + print("IsaacTeleop control keys: [B] start/resume [P] pause [R] reset") + return keyboard + except Exception as e: + logger.warning(f"Control keyboard unavailable ({e}); teleop still auto-starts without --xr") + return None + + +def main() -> None: # noqa: C901 + """ + Run teleoperation with an Isaac Lab manipulation environment. Creates the environment, sets up teleoperation interfaces and callbacks, and runs the main simulation loop until the application is closed. @@ -87,9 +258,16 @@ def main() -> None: Returns: None """ - # parse configuration - env_cfg = parse_env_cfg(args_cli.task, device=args_cli.device, num_envs=args_cli.num_envs) + # Resolve the task configuration through Hydra so CLI presets are applied. + env_cfg, _ = resolve_task_config(args_cli.task, "") + env_cfg.sim.device = args_cli.device + env_cfg.scene.num_envs = args_cli.num_envs env_cfg.env_name = args_cli.task + if not isinstance(env_cfg, ManagerBasedRLEnvCfg): + raise ValueError( + "Teleoperation is only supported for ManagerBasedRLEnv environments. " + f"Received environment config type: {type(env_cfg).__name__}" + ) # modify configuration env_cfg.terminations.time_out = None if "Lift" in args_cli.task: @@ -98,11 +276,44 @@ def main() -> None: # add termination condition for reaching the goal otherwise the environment won't reset env_cfg.terminations.object_reached_goal = DoneTerm(func=mdp.object_reached_goal) + # When --teleop_device is explicitly provided, use the legacy teleop_devices path + # even if isaac_teleop is configured. Otherwise prefer isaac_teleop when available. + teleop_device_explicitly_set = args_cli.teleop_device is not None + use_isaac_teleop = ( + not teleop_device_explicitly_set and hasattr(env_cfg, "isaac_teleop") and env_cfg.isaac_teleop is not None + ) + + from isaaclab_teleop import XrCameraFeedSession + + camera_feed_session = XrCameraFeedSession.prepare( + env_cfg, + enabled=args_cli.xr and use_isaac_teleop, + camera_rendering_enabled=not args_cli.disable_external_cameras, + ) + + # XR-rendering setup (camera removal + DLSS) is only needed for the Kit XR + # path. Without --xr, IsaacTeleop runs standalone (I/O only) and renders + # normally, so gate on --xr alone. if args_cli.xr: - # External cameras are not supported with XR teleop - # Check for any camera configs and disable them - env_cfg = remove_camera_configs(env_cfg) - env_cfg.sim.render.antialiasing_mode = "DLSS" + # Keep camera configs when external cameras are enabled (defaulted on); otherwise + # strip them so the XR headset view is the sole render product. + if args_cli.disable_external_cameras: + env_cfg = remove_camera_configs(env_cfg) + # Apply the RTX/DLSS global settings when an RTX render pipeline will run (Kit visualizer, + # external cameras, or XR). ``apply_isaac_rtx_global_settings`` uses ``omni.replicator``, + # which some experiences do not preload, so ensure it is loaded first. + if _rtx_rendering_requested(args_cli): + _ensure_replicator_loaded() + apply_isaac_rtx_global_settings( + IsaacRtxRendererGlobalSettingsCfg( + antialiasing_mode="DLSS", + carb_settings=( + {"/rtx/dldenoiser/responsiveDenoising": True} + if camera_feed_session.requires_responsive_denoising + else None + ), + ), + ) try: # create environment @@ -170,47 +381,64 @@ def stop_teleoperation() -> None: "RESET": reset_recording_instance, } - # For hand tracking devices, add additional callbacks - if args_cli.xr: - # Default to inactive for hand tracking - teleoperation_active = False + # For XR devices (hand tracking or IsaacTeleop), default to inactive. Without + # --xr, teleop is started locally (see ``request_start`` below) rather than by + # a headset, so it still begins running -- but it flows through the same state + # machine, so keyboard/host pause/resume keeps working. + if use_isaac_teleop or args_cli.xr: + teleoperation_active = env_cfg.isaac_teleop.teleoperation_active_default if use_isaac_teleop else False else: # Always active for other devices teleoperation_active = True - # Create teleop device from config if present, otherwise create manually + # Create teleop device based on configuration teleop_interface = None + try: - if hasattr(env_cfg, "teleop_devices") and args_cli.teleop_device in env_cfg.teleop_devices.devices: - teleop_interface = create_teleop_device( - args_cli.teleop_device, env_cfg.teleop_devices.devices, teleoperation_callbacks - ) - else: - logger.warning( - f"No teleop device '{args_cli.teleop_device}' found in environment config. Creating default." + if use_isaac_teleop: + from isaaclab_teleop import create_isaac_teleop_device, poll_control_events + + teleop_interface = create_isaac_teleop_device( + env_cfg.isaac_teleop, + sim_device=args_cli.device, + callbacks=teleoperation_callbacks, + cloudxr_env_file=_resolve_cloudxr_env(args_cli.cloudxr_env, args_cli.xr), + auto_launch_cloudxr=args_cli.auto_launch_cloudxr, + enable_debug_visualization=args_cli.enable_debug_visualization, + use_kit_xr_bridge=args_cli.xr, + haptic_cfg=getattr(env_cfg, "haptic_feedback", None), ) - # Create fallback teleop device - sensitivity = args_cli.sensitivity - if args_cli.teleop_device.lower() == "keyboard": - teleop_interface = Se3Keyboard( - Se3KeyboardCfg(pos_sensitivity=0.05 * sensitivity, rot_sensitivity=0.05 * sensitivity) - ) - elif args_cli.teleop_device.lower() == "spacemouse": - teleop_interface = Se3SpaceMouse( - Se3SpaceMouseCfg(pos_sensitivity=0.05 * sensitivity, rot_sensitivity=0.05 * sensitivity) - ) - elif args_cli.teleop_device.lower() == "gamepad": - teleop_interface = Se3Gamepad( - Se3GamepadCfg(pos_sensitivity=0.1 * sensitivity, rot_sensitivity=0.1 * sensitivity) + + elif teleop_device_explicitly_set: + device_name = args_cli.teleop_device + if hasattr(env_cfg, "teleop_devices") and device_name in env_cfg.teleop_devices.devices: + teleop_interface = create_teleop_device( + device_name, env_cfg.teleop_devices.devices, teleoperation_callbacks ) else: - logger.error(f"Unsupported teleop device: {args_cli.teleop_device}") - logger.error("Supported devices: keyboard, spacemouse, gamepad, handtracking") - env.close() - simulation_app.close() - return - - # Add callbacks to fallback device + teleop_interface = _create_builtin_device(device_name, args_cli.sensitivity) + if teleop_interface is None: + logger.error( + f"--teleop_device={device_name} was passed but no matching entry exists in" + " env_cfg.teleop_devices and it is not a built-in device name. Either remove" + " --teleop_device to use the IsaacTeleop pipeline, or add a" + f" '{device_name}' entry under teleop_devices in the environment config." + " Built-in devices: keyboard, spacemouse, gamepad." + ) + env.close() + simulation_app.close() + return + for key, callback in teleoperation_callbacks.items(): + try: + teleop_interface.add_callback(key, callback) + except (ValueError, TypeError) as e: + logger.warning(f"Failed to add callback for key {key}: {e}") + else: + # No --teleop_device and no isaac_teleop: fall back to keyboard + sensitivity = args_cli.sensitivity + teleop_interface = Se3Keyboard( + Se3KeyboardCfg(pos_sensitivity=0.05 * sensitivity, rot_sensitivity=0.05 * sensitivity) + ) for key, callback in teleoperation_callbacks.items(): try: teleop_interface.add_callback(key, callback) @@ -230,36 +458,82 @@ def stop_teleoperation() -> None: print(f"Using teleop device: {teleop_interface}") - # reset environment - env.reset() - teleop_interface.reset() - - print("Teleoperation started. Press 'R' to reset the environment.") - - # simulate environment - while simulation_app.is_running(): - try: - # run everything in inference mode - with torch.inference_mode(): - # get device command - action = teleop_interface.advance() - - # Only apply teleop commands when active - if teleoperation_active: - # process actions - actions = action.repeat(env.num_envs, 1) - # apply actions - env.step(actions) - else: - env.sim.render() - - if should_reset_recording_instance: - env.reset() - should_reset_recording_instance = False - print("Environment reset complete") - except Exception as e: - logger.error(f"Error during simulation step: {e}") - break + # Optional controller haptics: no-ops unless the env declares a + # ``haptic_feedback`` config and the device can render it (IsaacTeleop). + haptic_update, haptic_stop = _make_haptic_io(env, teleop_interface, env_cfg, use_isaac_teleop) + + # Optional keyboard for headset-free IsaacTeleop control. Kept in a local so its + # carb input subscription is not garbage-collected; a headless run auto-starts + # (in ``run_loop``) without it. + control_keyboard = _make_control_keyboard(teleop_interface, use_isaac_teleop, app_launcher.has_window) # noqa: F841 + + def run_loop(): + """Inner function to run the teleop loop with access to nonlocal variables.""" + nonlocal should_reset_recording_instance, teleoperation_active + + # reset environment + env.reset() + teleop_interface.reset() + + # Without --xr there is no headset to send START, so start locally ([B]/[P] can + # still pause/resume). The reset() above is a host reset (a pure pulse), so it does + # not cancel this start. + if use_isaac_teleop and not args_cli.xr: + teleop_interface.request_start() + + stack_name = "IsaacTeleop" if use_isaac_teleop else "native" + print(f"{stack_name} teleoperation started. Press 'R' to reset the environment.") + + # simulate environment + while simulation_app.is_running(): + try: + # run everything in inference mode + with torch.inference_mode(): + # get device command + action = teleop_interface.advance() + + if use_isaac_teleop: + ctrl = poll_control_events(teleop_interface) + if ctrl.is_active is not None: + teleoperation_active = ctrl.is_active + if ctrl.should_reset: + should_reset_recording_instance = True + + # action is None when IsaacTeleop session hasn't started yet + # (e.g. waiting for user to click "Start AR") + if action is None: + env.sim.render() + haptic_stop() + elif teleoperation_active: + # process actions + actions = action.repeat(env.num_envs, 1) + # apply actions + env.step(actions) + # render controller haptics from post-step contact forces + haptic_update() + else: + env.sim.render() + # not stepping: zero haptics so a paused grip stops buzzing + haptic_stop() + + if should_reset_recording_instance: + env.reset() + teleop_interface.reset() + camera_feed_session.refresh() + should_reset_recording_instance = False + print("Environment reset complete") + except Exception as e: + logger.error(f"Error during simulation step: {e}") + break + + # Run the teleoperation loop + # IsaacTeleop requires a context manager, native devices don't + if use_isaac_teleop: + with teleop_interface, camera_feed_session.bind(env): + run_loop() + else: + with camera_feed_session.bind(env): + run_loop() # close the simulator env.close() @@ -269,5 +543,7 @@ def stop_teleoperation() -> None: if __name__ == "__main__": # run the main function main() - # close sim app + # env.close() already closes the USD stage via sim.clear_instance(). + # Pump the event loop so the viewport processes closure, then close the app. + simulation_app.update() simulation_app.close() diff --git a/scripts/environments/zero_agent.py b/scripts/environments/zero_agent.py index 88aa98e7..a3519dcc 100644 --- a/scripts/environments/zero_agent.py +++ b/scripts/environments/zero_agent.py @@ -1,73 +1,29 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -"""Script to run an environment with zero action agent.""" +"""Zero-action agent executable for Isaac Lab environments.""" -"""Launch Isaac Sim Simulator first.""" - -import argparse - -from isaaclab.app import AppLauncher - -# add argparse arguments -parser = argparse.ArgumentParser(description="Zero agent for Isaac Lab environments.") -parser.add_argument( - "--disable_fabric", action="store_true", default=False, help="Disable fabric and use USD I/O operations." -) -parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") -parser.add_argument("--task", type=str, default=None, help="Name of the task.") -# append AppLauncher cli args -AppLauncher.add_app_launcher_args(parser) -# parse the arguments -args_cli = parser.parse_args() +# PLACEHOLDER: Extension template (do not remove this comment) -# launch omniverse app -app_launcher = AppLauncher(args_cli) -simulation_app = app_launcher.app +# Warp captures ``enable_backward`` when a module is created, which happens at import +# time, so it has to be set before importing anything that defines Warp kernels. +# Isaac Lab does not use Warp autodiff; skipping adjoint codegen roughly halves the +# time spent building kernels on a cold kernel cache. +import warp as wp -"""Rest everything follows.""" +wp.config.enable_backward = False -import gymnasium as gym -import torch +from isaaclab_rl.entrypoints import run_zero_agent_cli # noqa: E402 -import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -from isaaclab_tasks.utils import parse_env_cfg - -# PLACEHOLDER: Extension template (do not remove this comment) - - -def main(): - """Zero actions agent with Isaac Lab environment.""" - # parse configuration - env_cfg = parse_env_cfg( - args_cli.task, device=args_cli.device, num_envs=args_cli.num_envs, use_fabric=not args_cli.disable_fabric - ) - # create environment - env = gym.make(args_cli.task, cfg=env_cfg) - # print info (this is vectorized environment) - print(f"[INFO]: Gym observation space: {env.observation_space}") - print(f"[INFO]: Gym action space: {env.action_space}") - # reset environment - env.reset() - # simulate environment - while simulation_app.is_running(): - # run everything in inference mode - with torch.inference_mode(): - # compute zero actions - actions = torch.zeros(env.action_space.shape, device=env.unwrapped.device) - # apply actions - env.step(actions) - # close the simulator - env.close() +def main(argv: list[str] | None = None) -> int: + """Run an environment with a zero-action agent.""" + return run_zero_agent_cli(argv) if __name__ == "__main__": - # run the main function - main() - # close sim app - simulation_app.close() + raise SystemExit(main()) diff --git a/scripts/imitation_learning/isaaclab_mimic/annotate_demos.py b/scripts/imitation_learning/isaaclab_mimic/annotate_demos.py index f67f0bef..c53dd425 100644 --- a/scripts/imitation_learning/isaaclab_mimic/annotate_demos.py +++ b/scripts/imitation_learning/isaaclab_mimic/annotate_demos.py @@ -1,7 +1,7 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2024-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # -# SPDX-License-Identifier: BSD-3-Clause +# SPDX-License-Identifier: Apache-2.0 """ Script to add mimic annotations to demos to be used as source demos for mimic dataset generation. @@ -11,6 +11,7 @@ import math from isaaclab.app import AppLauncher +from isaaclab.utils.string import list_intersection, string_to_callable # Launching Isaac Sim Simulator first. @@ -28,12 +29,6 @@ help="File name of the annotated output dataset file.", ) parser.add_argument("--auto", action="store_true", default=False, help="Automatically annotate subtasks.") -parser.add_argument( - "--enable_pinocchio", - action="store_true", - default=False, - help="Enable Pinocchio.", -) parser.add_argument( "--annotate_subtask_start_signals", action="store_true", @@ -41,32 +36,41 @@ help="Enable annotating start points of subtasks.", ) +parser.add_argument("--external_callback", default=None, help="Fully qualified path to an externally defined callback.") # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments -args_cli = parser.parse_args() - -if args_cli.enable_pinocchio: - # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim - # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter - import pinocchio # noqa: F401 +args_cli, remaining_args = parser.parse_known_args() # launch the simulator app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app +# Call an external callback if requested. +remaining_args_env_registration = None +if args_cli.external_callback: + external_callback_function = string_to_callable(args_cli.external_callback, separator=".") + remaining_args_env_registration = external_callback_function() + +# Extras of the form "key=value" are Hydra-style task config overrides forwarded to +# parse_env_cfg(); everything else must be consumed by the external callback or is an error. +hydra_overrides = [arg for arg in remaining_args if "=" in arg] +unrecognized_args = list_intersection( + [arg for arg in remaining_args if arg not in hydra_overrides], remaining_args_env_registration +) +if unrecognized_args: + parser.error(f"unrecognized arguments: {' '.join(unrecognized_args)}") + """Rest everything follows.""" import contextlib -import gymnasium as gym import os + +import gymnasium as gym import torch import isaaclab_mimic.envs # noqa: F401 -if args_cli.enable_pinocchio: - import isaaclab_mimic.envs.pinocchio_envs # noqa: F401 - # Only enables inputs if this script is NOT headless mode if not args_cli.headless and not os.environ.get("HEADLESS", 0): from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg @@ -196,7 +200,7 @@ def main(): if env_name is None: raise ValueError("Task/env name was not specified nor found in the dataset.") - env_cfg = parse_env_cfg(env_name, device=args_cli.device, num_envs=1) + env_cfg = parse_env_cfg(env_name, device=args_cli.device, num_envs=1, overrides=hydra_overrides) env_cfg.env_name = env_name diff --git a/scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py b/scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py index 57381af7..94c3681c 100644 --- a/scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py +++ b/scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py @@ -1,7 +1,7 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2024-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # -# SPDX-License-Identifier: BSD-3-Clause +# SPDX-License-Identifier: Apache-2.0 """ Script to record teleoperated demos and run mimic dataset generation in real-time. @@ -62,8 +62,8 @@ ) # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) -# parse the arguments -args_cli = parser.parse_args() +# parse the arguments, forwarding unrecognized ones as Hydra-style task config overrides +args_cli, hydra_overrides = parser.parse_known_args() # launch the simulator app_launcher = AppLauncher(args_cli) @@ -73,11 +73,12 @@ import asyncio import contextlib -import gymnasium as gym -import numpy as np import os import random import time + +import gymnasium as gym +import numpy as np import torch from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg, Se3SpaceMouse, Se3SpaceMouseCfg @@ -302,7 +303,6 @@ def env_loop(env, env_action_queue, shared_datagen_info_pool, asyncio_event_loop is_first_print = True with contextlib.suppress(KeyboardInterrupt) and torch.inference_mode(): while True: - actions = torch.zeros(env.unwrapped.action_space.shape) # get actions from all the data generators @@ -372,7 +372,7 @@ def main(): raise ValueError("Task/env name was not specified nor found in the dataset.") # parse configuration - env_cfg = parse_env_cfg(env_name, device=args_cli.device, num_envs=num_envs) + env_cfg = parse_env_cfg(env_name, device=args_cli.device, num_envs=num_envs, overrides=hydra_overrides) env_cfg.env_name = env_name # extract success checking function to invoke manually diff --git a/scripts/imitation_learning/isaaclab_mimic/generate_dataset.py b/scripts/imitation_learning/isaaclab_mimic/generate_dataset.py index fcc85f84..b982906b 100644 --- a/scripts/imitation_learning/isaaclab_mimic/generate_dataset.py +++ b/scripts/imitation_learning/isaaclab_mimic/generate_dataset.py @@ -1,18 +1,18 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2024-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # -# SPDX-License-Identifier: BSD-3-Clause +# SPDX-License-Identifier: Apache-2.0 """ Main data generation script. """ - """Launch Isaac Sim Simulator first.""" import argparse from isaaclab.app import AppLauncher +from isaaclab.utils.string import list_intersection, string_to_callable # add argparse arguments parser = argparse.ArgumentParser(description="Generate demonstrations for Isaac Lab environments.") @@ -31,57 +31,66 @@ parser.add_argument( "--pause_subtask", action="store_true", - help="pause after every subtask during generation for debugging - only useful with render flag", + help="Pause after every subtask during generation for debugging - only useful with render flag", ) parser.add_argument( - "--enable_pinocchio", + "--use_skillgen", action="store_true", default=False, - help="Enable Pinocchio.", + help="Use skillgen to generate motion trajectories", ) parser.add_argument( - "--use_skillgen", + "--disable_dataset_compression", action="store_true", default=False, - help="use skillgen to generate motion trajectories", + help="Disables dataset compression", +) +parser.add_argument( + "--external_callback", + default=None, + help="Fully qualified path to an externally defined callback.", ) + # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments -args_cli = parser.parse_args() +args_cli, remaining_args = parser.parse_known_args() -if args_cli.enable_pinocchio: - # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim - # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter - import pinocchio # noqa: F401 - -# launch the simulator -app_launcher = AppLauncher(args_cli) +# Mimic environments may use camera observations or an RTX renderer. Request +# rendering support here so callers do not need a legacy CLI flag. +app_launcher = AppLauncher(args_cli, enable_cameras=True) simulation_app = app_launcher.app +import uwlab_tasks # noqa: F401 + +# Call an external callback if requested. +remaining_args_env_registration = None +if args_cli.external_callback: + external_callback_function = string_to_callable(args_cli.external_callback, separator=".") + remaining_args_env_registration = external_callback_function() + +# Error on unrecognized arguments. +unrecognized_args = list_intersection(remaining_args, remaining_args_env_registration) +if unrecognized_args: + parser.error(f"unrecognized arguments: {' '.join(unrecognized_args)}") + """Rest everything follows.""" import asyncio -import gymnasium as gym import inspect import logging -import numpy as np import random + +import gymnasium as gym +import numpy as np import torch from isaaclab.envs import ManagerBasedRLMimicEnv import isaaclab_mimic.envs # noqa: F401 - -if args_cli.enable_pinocchio: - import isaaclab_mimic.envs.pinocchio_envs # noqa: F401 - from isaaclab_mimic.datagen.generation import env_loop, setup_async_generation, setup_env_config from isaaclab_mimic.datagen.utils import get_env_name_from_dataset, setup_output_paths -import isaaclab_tasks # noqa: F401 -import uwlab_tasks # noqa: F401 - # import logger logger = logging.getLogger(__name__) @@ -104,6 +113,7 @@ def main(): num_envs=num_envs, device=args_cli.device, generation_num_trials=args_cli.generation_num_trials, + dataset_compression=not args_cli.disable_dataset_compression, ) # Create environment @@ -128,72 +138,77 @@ def main(): env.reset() motion_planners = None - if args_cli.use_skillgen: - from isaaclab_mimic.motion_planners.curobo.curobo_planner import CuroboPlanner - from isaaclab_mimic.motion_planners.curobo.curobo_planner_cfg import CuroboPlannerCfg - - # Create one motion planner per environment - motion_planners = {} - for env_id in range(num_envs): - print(f"Initializing motion planner for environment {env_id}") - # Create a config instance from the task name - planner_config = CuroboPlannerCfg.from_task_name(env_name) - - # Ensure visualization is only enabled for the first environment - # If not, sphere and plan visualization will be too slow in isaac lab - # It is efficient to visualize the spheres and plan for the first environment in rerun - if env_id != 0: - planner_config.visualize_spheres = False - planner_config.visualize_plan = False - - motion_planners[env_id] = CuroboPlanner( - env=env, - robot=env.scene["robot"], - config=planner_config, # Pass the config object - env_id=env_id, # Pass environment ID - ) - - env.cfg.datagen_config.use_skillgen = True - - # Setup and run async data generation - async_components = setup_async_generation( - env=env, - num_envs=args_cli.num_envs, - input_file=args_cli.input_file, - success_term=success_term, - pause_subtask=args_cli.pause_subtask, - motion_planners=motion_planners, # Pass the motion planners dictionary - ) - try: - data_gen_tasks = asyncio.ensure_future(asyncio.gather(*async_components["tasks"])) - env_loop( - env, - async_components["reset_queue"], - async_components["action_queue"], - async_components["info_pool"], - async_components["event_loop"], + if args_cli.use_skillgen: + from isaaclab_mimic.motion_planners.curobo.curobo_planner import CuroboPlanner + from isaaclab_mimic.motion_planners.curobo.curobo_planner_cfg import CuroboPlannerCfg + + # Create one motion planner per environment + motion_planners = {} + for env_id in range(num_envs): + print(f"Initializing motion planner for environment {env_id}") + # Create a config instance from the task name + planner_config = CuroboPlannerCfg.from_task_name(env_name) + + # Ensure visualization is only enabled for the first environment + # If not, sphere and plan visualization will be too slow in isaac lab + # It is efficient to visualize the spheres and plan for the first environment in rerun + if env_id != 0: + planner_config.visualize_spheres = False + planner_config.visualize_plan = False + + motion_planners[env_id] = CuroboPlanner( + env=env, + robot=env.scene["robot"], + config=planner_config, # Pass the config object + env_id=env_id, # Pass environment ID + ) + + env.cfg.datagen_config.use_skillgen = True + + # Setup and run async data generation + async_components = setup_async_generation( + env=env, + num_envs=args_cli.num_envs, + input_file=args_cli.input_file, + success_term=success_term, + pause_subtask=args_cli.pause_subtask, + motion_planners=motion_planners, # Pass the motion planners dictionary ) - except asyncio.CancelledError: - print("Tasks were cancelled.") - finally: - # Cancel all async tasks when env_loop finishes - data_gen_tasks.cancel() + try: - # Wait for tasks to be cancelled - async_components["event_loop"].run_until_complete(data_gen_tasks) + data_gen_tasks = asyncio.ensure_future(asyncio.gather(*async_components["tasks"])) + env_loop( + env, + async_components["reset_queue"], + async_components["action_queue"], + async_components["info_pool"], + async_components["event_loop"], + data_gen_tasks=data_gen_tasks, + ) except asyncio.CancelledError: - print("Remaining async tasks cancelled and cleaned up.") - except Exception as e: - print(f"Error cancelling remaining async tasks: {e}") - # Cleanup of motion planners and their visualizers - if motion_planners is not None: - for env_id, planner in motion_planners.items(): - if getattr(planner, "plan_visualizer", None) is not None: - print(f"Closing plan visualizer for environment {env_id}") - planner.plan_visualizer.close() - planner.plan_visualizer = None - motion_planners.clear() + print("Tasks were cancelled.") + finally: + # Cancel all async tasks when env_loop finishes + data_gen_tasks.cancel() + try: + # Wait for tasks to be cancelled + async_components["event_loop"].run_until_complete(data_gen_tasks) + except asyncio.CancelledError: + print("Remaining async tasks cancelled and cleaned up.") + except Exception as e: + print(f"Error cancelling remaining async tasks: {e}") + # Cleanup of motion planners and their visualizers + if motion_planners is not None: + for env_id, planner in motion_planners.items(): + if getattr(planner, "plan_visualizer", None) is not None: + print(f"Closing plan visualizer for environment {env_id}") + planner.plan_visualizer.close() + planner.plan_visualizer = None + motion_planners.clear() + finally: + # Close env after async tasks are done so success_term is never called on a closed env + env.close() if __name__ == "__main__": diff --git a/scripts/imitation_learning/locomanipulation_sdg/generate_data.py b/scripts/imitation_learning/locomanipulation_sdg/generate_data.py index f9ed8325..f7191630 100644 --- a/scripts/imitation_learning/locomanipulation_sdg/generate_data.py +++ b/scripts/imitation_learning/locomanipulation_sdg/generate_data.py @@ -1,19 +1,15 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """Script to replay demonstrations with Isaac Lab environments.""" -"""Launch Isaac Sim Simulator first.""" - - import argparse import os from isaaclab.app import AppLauncher -# Launch Isaac Lab parser = argparse.ArgumentParser(description="Locomanipulation SDG") parser.add_argument("--task", type=str, help="The Isaac Lab locomanipulation SDG task to load for data generation.") parser.add_argument("--dataset", type=str, help="The static manipulation dataset recorded via teleoperation.") @@ -30,14 +26,17 @@ "--navigate_step", type=int, help=( - "The step index in the input recording where the robot is ready to navigate. Aka, where it has finished" - " lifting the object" + "The step index in the input recording where the robot is ready to navigate. Aka, where it has finished" + " lifting the object." ), ) -parser.add_argument("--demo", type=str, default=None, help="The demo in the input dataset to use.") +parser.add_argument("--demo", type=str, default=None, help="The demo in the input dataset to use, e.g. 'demo_0'") parser.add_argument("--num_runs", type=int, default=1, help="The number of trajectories to generate.") parser.add_argument( - "--draw_visualization", type=bool, default=False, help="Draw the occupancy map and path planning visualization." + "--draw_visualization", + action="store_true", + default=False, + help="Draw the occupancy map and path planning visualization.", ) parser.add_argument( "--angular_gain", @@ -88,42 +87,79 @@ ) parser.add_argument( "--randomize_placement", - type=bool, - default=True, + action="store_true", + default=False, help="Whether or not to randomize the placement of fixtures in the scene upon environment initialization.", ) parser.add_argument( - "--enable_pinocchio", + "--background_usd_path", + type=str, + default=None, + help="Path to the USD file for the background asset", +) +parser.add_argument( + "--background_occupancy_yaml_file", + type=str, + default=None, + help="Path to the occupancy map YAML file for the background asset", +) +parser.add_argument( + "--high_res_video", action="store_true", default=False, - help="Enable Pinocchio.", + help="Whether to use high resolution video for the robot's POV camera.", +) +parser.add_argument( + "--seed", + type=int, + default=None, + help="Random seed for reproducibility.", +) +parser.add_argument( + "--sensor_camera_view", + action="store_true", + default=False, + help="Set the Sim GUI viewport to the robot_pov_cam sensor view at the start of each episode.", ) -AppLauncher.add_app_launcher_args(parser) -args_cli = parser.parse_args() -if args_cli.enable_pinocchio: - # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim - # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter - import pinocchio # noqa: F401 +AppLauncher.add_app_launcher_args(parser) +# forward unrecognized args as Hydra-style task config overrides +args_cli, hydra_overrides = parser.parse_known_args() app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app import enum -import gymnasium as gym import random +from dataclasses import dataclass + +import gymnasium as gym +import numpy as np import torch +import warp as wp import omni.kit +import omni.kit.viewport.utility +import omni.usd +from isaaclab.managers import DatasetExportMode from isaaclab.utils import configclass from isaaclab.utils.datasets import EpisodeData, HDF5DatasetFileHandler +from isaaclab.utils.math import convert_quat +from isaaclab.utils.seed import configure_seed import isaaclab_mimic.locomanipulation_sdg.envs # noqa: F401 -from isaaclab_mimic.locomanipulation_sdg.data_classes import LocomanipulationSDGOutputData +from isaaclab_mimic.locomanipulation_sdg.data_classes import ( + LocomanipulationSDGOutputData, +) + +if args_cli.seed is not None: + configure_seed(args_cli.seed) + from isaaclab_mimic.locomanipulation_sdg.envs.locomanipulation_sdg_env import LocomanipulationSDGEnv from isaaclab_mimic.locomanipulation_sdg.occupancy_map_utils import ( OccupancyMap, + OccupancyMapDataValue, merge_occupancy_maps, occupancy_map_add_to_stage, ) @@ -169,7 +205,7 @@ class LocomanipulationSDGControlConfig: linear_max: float = 1.0 """Maximum allowed linear velocity (m/s)""" - distance_threshold: float = 0.1 + distance_threshold: float = 0.2 """Distance threshold for state transitions (m)""" following_offset: float = 0.6 @@ -178,12 +214,28 @@ class LocomanipulationSDGControlConfig: angle_threshold: float = 0.2 """Angular threshold for orientation control (rad)""" - approach_distance: float = 1.0 + approach_distance: float = 0.5 """Buffer distance from final goal (m)""" +@dataclass +class NavigationScene: + """Navigation scene data class.""" + + """The occupancy map of the navigation scene.""" + occupancy_map: OccupancyMap + """The base path helper of the navigation scene.""" + base_path_helper: ParameterizedPath + """The base goal of the navigation scene.""" + base_goal: RelativePose + """The approach goal of the navigation scene.""" + base_goal_approach: RelativePose + + def compute_navigation_velocity( - current_pose: torch.Tensor, target_xy: torch.Tensor, config: LocomanipulationSDGControlConfig + current_pose: torch.Tensor, + target_xy: torch.Tensor, + config: LocomanipulationSDGControlConfig, ) -> tuple[torch.Tensor, torch.Tensor]: """Compute linear and angular velocities for navigation control. @@ -203,8 +255,8 @@ def compute_navigation_velocity( delta_distance = torch.sqrt(torch.sum(delta_xy**2)) target_yaw = torch.arctan2(delta_xy[1], delta_xy[0]) - delta_yaw = target_yaw - current_yaw # Normalize angle to [-π, π] + delta_yaw = target_yaw - current_yaw delta_yaw = (delta_yaw + torch.pi) % (2 * torch.pi) - torch.pi # Compute control commands @@ -222,7 +274,7 @@ def load_and_transform_recording_data( recording_step: int, reference_pose: torch.Tensor, target_pose: torch.Tensor, -) -> tuple[torch.Tensor, torch.Tensor]: +) -> tuple[torch.Tensor | None, torch.Tensor | None]: """Load recording data and transform hand targets to current reference frame. Args: @@ -245,12 +297,130 @@ def load_and_transform_recording_data( return left_hand_pose, right_hand_pose +def sync_simulation_state(env: LocomanipulationSDGEnv): + """Push USD pose updates into physics, advance the sim, and sync back + + Args: + env: The locomanipulation SDG environment + """ + env.scene.write_data_to_sim() + env.sim.step(render=False) + env.scene.update(dt=env.physics_dt) + + +def _set_sensor_camera_view(): + """Set the Sim GUI viewport to display the robot_pov_cam sensor view.""" + viewport = omni.kit.viewport.utility.get_active_viewport() + if viewport is not None: + cam_prim_path = "/World/envs/env_0/Robot/torso_link/d435_link/camera" + viewport.set_active_camera(cam_prim_path) + + +def project_robot_state_into_env(env: LocomanipulationSDGEnv, input_episode_data: EpisodeData) -> torch.Tensor: + """Project the recorded robot pose and joint state into the current environment. + + Args: + env: The locomanipulation SDG environment + input_episode_data: Input episode data + + Returns: + The new robot pose + """ + initial_state = env.load_input_data(input_episode_data, 0) + recording_initial_state = input_episode_data.get_initial_state() + + object = env.scene["object"] + current_object_pose = torch.cat( + [ + torch.as_tensor(object.data.root_pos_w.torch[0:1], device=env.device, dtype=torch.float32), + torch.as_tensor(object.data.root_quat_w.torch[0:1], device=env.device, dtype=torch.float32), + ], + dim=-1, + ) # (1, 7) + + new_robot_pose = transform_mul( + current_object_pose, + transform_mul( + transform_inv(initial_state.object_pose.to(env.device)), + initial_state.base_pose.to(env.device), + ), + ) + + env.scene["robot"].write_root_pose_to_sim_index(root_pose=new_robot_pose, env_ids=[0]) + env.scene["robot"].write_root_velocity_to_sim_index( + root_velocity=torch.zeros((1, 6), device=env.device), env_ids=[0] + ) + # Update default root pose and velocity for correct state on reset + default_pose = env.scene["robot"].data.default_root_pose.torch.clone() + default_pose[0] = new_robot_pose[0] + env.scene["robot"].data.default_root_pose.warp.assign( + wp.from_torch(default_pose.to(env.device).contiguous()).view(wp.transformf) + ) + default_vel = env.scene["robot"].data.default_root_vel.torch.clone() + default_vel[0] = torch.zeros(6, device=env.device) + env.scene["robot"].data.default_root_vel.warp.assign(wp.from_torch(default_vel.to(env.device).contiguous())) + + robot_state = recording_initial_state["articulation"]["robot"] + joint_position = robot_state["joint_position"][0].to(env.device) + joint_velocity = robot_state["joint_velocity"][0].to(env.device) + + # Update default joint positions and velocities for correct state on reset + default_joint_pos = env.scene["robot"].data.default_joint_pos.torch.clone() + default_joint_pos[0] = joint_position + env.scene["robot"].data.default_joint_pos.warp.assign(wp.from_torch(default_joint_pos.to(env.device).contiguous())) + default_joint_vel = env.scene["robot"].data.default_joint_vel.torch.clone() + default_joint_vel[0] = joint_velocity + env.scene["robot"].data.default_joint_vel.warp.assign(wp.from_torch(default_joint_vel.to(env.device).contiguous())) + env.scene["robot"].write_joint_position_to_sim_index(position=joint_position[None, :], env_ids=[0]) + env.scene["robot"].write_joint_velocity_to_sim_index(velocity=joint_velocity[None, :], env_ids=[0]) + + return new_robot_pose + + +def project_object_state_into_env(env: LocomanipulationSDGEnv, input_episode_data: EpisodeData) -> torch.Tensor: + """Project the recorded object pose into the current environment. + + Args: + env: The locomanipulation SDG environment + input_episode_data: Input episode data + + Returns: + The new object pose + """ + initial_state = env.load_input_data(input_episode_data, 0) + + new_object_pose = transform_mul( + env.get_start_fixture().get_pose(), + transform_mul( + transform_inv(initial_state.fixture_pose.to(env.device)), + initial_state.object_pose.to(env.device), + ), + ) + + env.scene["object"].write_root_pose_to_sim_index(root_pose=new_object_pose, env_ids=[0]) + env.scene["object"].write_root_velocity_to_sim_index( + root_velocity=torch.zeros((1, 6), device=env.device), env_ids=[0] + ) + # Update default root pose and velocity for correct state on reset + default_pose = env.scene["object"].data.default_root_pose.torch.clone() + default_pose[0] = new_object_pose[0] + env.scene["object"].data.default_root_pose.warp.assign( + wp.from_torch(default_pose.to(env.device).contiguous()).view(wp.transformf) + ) + default_vel = env.scene["object"].data.default_root_vel.torch.clone() + default_vel[0] = torch.zeros(6, device=env.device) + env.scene["object"].data.default_root_vel.warp.assign(wp.from_torch(default_vel.to(env.device).contiguous())) + + return new_object_pose + + def setup_navigation_scene( env: LocomanipulationSDGEnv, input_episode_data: EpisodeData, approach_distance: float, randomize_placement: bool = True, -) -> tuple[OccupancyMap, ParameterizedPath, RelativePose, RelativePose]: + draw_visualization: bool = False, +) -> NavigationScene | None: """Set up the navigation scene with occupancy map and path planning. Args: @@ -258,40 +428,101 @@ def setup_navigation_scene( input_episode_data: Input episode data approach_distance: Buffer distance from final goal randomize_placement: Whether to randomize fixture placement + draw_visualization: Whether to add occupancy map and path to the USD stage Returns: - Tuple of (occupancy_map, path_helper, base_goal, base_goal_approach) + NavigationScene or None if the navigation scene setup failed. """ - # Create base occupancy map - occupancy_map = merge_occupancy_maps([ - OccupancyMap.make_empty(start=(-7, -7), end=(7, 7), resolution=0.05), - env.get_start_fixture().get_occupancy_map(), - ]) - - # Randomize fixture placement if enabled - if randomize_placement: + + background_fixture = env.get_background_fixture() + if background_fixture is not None: + occupancy_map = background_fixture.get_occupancy_map() + fixtures = [env.get_start_fixture(), env.get_end_fixture()] + env.get_obstacle_fixtures() + if not randomize_placement: + raise ValueError("randomize_placement needs to be True when background_usd_path is provided") + else: + occupancy_map = merge_occupancy_maps( + [ + OccupancyMap.make_empty(start=(-7, -7), end=(7, 7), resolution=0.05), + env.get_start_fixture().get_occupancy_map(), + ] + ) + fixtures = [env.get_end_fixture()] + env.get_obstacle_fixtures() - for fixture in fixtures: - place_randomly(fixture, occupancy_map.buffered_meters(1.0)) - occupancy_map = merge_occupancy_maps([occupancy_map, fixture.get_occupancy_map()]) - # Compute goal poses from initial state + for fixture in fixtures: + if randomize_placement: + if not place_randomly(fixture, occupancy_map.buffered_meters(0.7), num_iter=5000): + print(f"Failed to randomize fixture placement for {fixture.entity_name}", flush=True) + return None + + sync_simulation_state(env) + occupancy_map = merge_occupancy_maps([occupancy_map, fixture.get_occupancy_map()]) + initial_state = env.load_input_data(input_episode_data, 0) + base_goal = RelativePose( - relative_pose=transform_mul(transform_inv(initial_state.fixture_pose), initial_state.base_pose), + relative_pose=transform_mul( + transform_inv(initial_state.fixture_pose.to(env.device)), + initial_state.base_pose.to(env.device), + ), parent=env.get_end_fixture(), ) base_goal_approach = RelativePose( - relative_pose=torch.tensor([-approach_distance, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]), parent=base_goal + relative_pose=torch.tensor([-approach_distance, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], device=env.device), + parent=base_goal, ) - # Plan navigation path - base_path = plan_path( - start=env.get_base(), end=base_goal_approach, occupancy_map=occupancy_map.buffered_meters(0.15) - ) + sync_simulation_state(env) + project_object_state_into_env(env, input_episode_data) + sync_simulation_state(env) + project_robot_state_into_env(env, input_episode_data) + # Flush physics writes so scene data buffers reflect the projected poses before path planning. + # Without this, env.get_base() would return the stale pre-projection position. + sync_simulation_state(env) + env.sim.render() + env.obs_buf = env.observation_manager.compute(update_history=True) + + nav_map = occupancy_map.buffered_meters(0.15) + nav_fs = nav_map.freespace_mask() + start_pos = env.get_base().get_pose()[0, :2].detach().cpu().numpy() + start_px = nav_map.world_to_pixel_numpy(start_pos[None])[0].astype(int) + sx, sy = int(start_px[0]), int(start_px[1]) + + # Clear the buffer zone around the robot's start pixel if it falls inside it. + # The robot stands adjacent to the start table so its position sits within the 0.15m buffer. + if 0 <= sy < nav_fs.shape[0] and 0 <= sx < nav_fs.shape[1] and not nav_fs[sy, sx]: + clear_r = int(0.15 / nav_map.resolution) + yy, xx = np.ogrid[: nav_map.data.shape[0], : nav_map.data.shape[1]] + nav_map.data[(xx - sx) ** 2 + (yy - sy) ** 2 <= clear_r**2] = OccupancyMapDataValue.FREESPACE + + base_path = plan_path(start=env.get_base(), end=base_goal_approach, occupancy_map=nav_map) + + if base_path is None: + return None + base_path_helper = ParameterizedPath(base_path) + if len(base_path_helper.points) <= 2: + print(f"Base path does not have enough points: {len(base_path_helper.points)} points", flush=True) + return None - return occupancy_map, base_path_helper, base_goal, base_goal_approach + sync_simulation_state(env) + + if draw_visualization: + occupancy_map_add_to_stage( + occupancy_map, + stage=omni.usd.get_context().get_stage(), + path="/OccupancyMap", + z_offset=0.01, + draw_path=base_path_helper.points, + ) + + return NavigationScene( + occupancy_map=occupancy_map, + base_path_helper=base_path_helper, + base_goal=base_goal, + base_goal_approach=base_goal_approach, + ) def handle_grasp_state( @@ -318,7 +549,7 @@ def handle_grasp_state( # Set control targets - robot stays stationary during grasping output_data.data_generation_state = int(LocomanipulationSDGDataGenerationState.GRASP_OBJECT) output_data.recording_step = recording_step - output_data.base_velocity_target = torch.tensor([0.0, 0.0, 0.0]) + output_data.base_velocity_target = torch.tensor([0.0, 0.0, 0.0], device=env.device) # Transform hand poses relative to object output_data.left_hand_pose_target = transform_relative_pose( @@ -366,7 +597,7 @@ def handle_lift_state( # Set control targets - robot stays stationary during lifting output_data.data_generation_state = int(LocomanipulationSDGDataGenerationState.LIFT_OBJECT) output_data.recording_step = recording_step - output_data.base_velocity_target = torch.tensor([0.0, 0.0, 0.0]) + output_data.base_velocity_target = torch.tensor([0.0, 0.0, 0.0], device=env.device) # Transform hand poses relative to base output_data.left_hand_pose_target = transform_relative_pose( @@ -425,7 +656,7 @@ def handle_navigate_state( # Set control targets output_data.data_generation_state = int(LocomanipulationSDGDataGenerationState.NAVIGATE) output_data.recording_step = recording_step - output_data.base_velocity_target = torch.tensor([linear_velocity, 0.0, angular_velocity]) + output_data.base_velocity_target = torch.tensor([linear_velocity, 0.0, angular_velocity], device=env.device) # Transform hand poses relative to base output_data.left_hand_pose_target = transform_relative_pose( @@ -479,7 +710,7 @@ def handle_approach_state( # Set control targets output_data.data_generation_state = int(LocomanipulationSDGDataGenerationState.APPROACH) output_data.recording_step = recording_step - output_data.base_velocity_target = torch.tensor([linear_velocity, 0.0, angular_velocity]) + output_data.base_velocity_target = torch.tensor([linear_velocity, 0.0, angular_velocity], device=env.device) # Transform hand poses relative to base output_data.left_hand_pose_target = transform_relative_pose( @@ -540,7 +771,7 @@ def handle_drop_off_state( # Set control targets output_data.data_generation_state = int(LocomanipulationSDGDataGenerationState.DROP_OFF_OBJECT) output_data.recording_step = recording_step - output_data.base_velocity_target = torch.tensor([linear_velocity, 0.0, angular_velocity]) + output_data.base_velocity_target = torch.tensor([linear_velocity, 0.0, angular_velocity], device=env.device) # Transform hand poses relative to end fixture output_data.left_hand_pose_target = transform_relative_pose( @@ -568,7 +799,7 @@ def populate_output_data( base_goal: RelativePose, base_goal_approach: RelativePose, base_path: torch.Tensor, -) -> None: +): """Populate remaining output data fields. Args: @@ -605,12 +836,13 @@ def replay( angular_gain: float = 2.0, linear_gain: float = 1.0, linear_max: float = 1.0, - distance_threshold: float = 0.1, + distance_threshold: float = 0.2, following_offset: float = 0.6, angle_threshold: float = 0.2, - approach_distance: float = 1.0, + approach_distance: float = 0.5, randomize_placement: bool = True, -) -> None: + sensor_camera_view: bool = False, +) -> bool: """Replay a locomanipulation SDG episode with state machine control. This function implements a state machine for locomanipulation SDG, where the robot: @@ -634,10 +866,20 @@ def replay( angle_threshold: Angular threshold for orientation control (rad) approach_distance: Buffer distance from final goal (m) randomize_placement: Whether to randomize obstacle placement + sensor_camera_view: Whether to set the Sim GUI viewport to the robot_pov_cam sensor view + + Returns: + True if the episode ended with success termination, False otherwise. """ + # Reset recorder manager to clear any leftover episode data from previous runs + # This prevents duplicate exports when reset_to calls record_pre_reset + env.recorder_manager.reset(env_ids=[0]) + # Initialize environment to starting state - env.reset_to(state=input_episode_data.get_initial_state(), env_ids=torch.tensor([0]), is_relative=True) + env.reset_to( + state=input_episode_data.get_initial_state(), env_ids=torch.tensor([0], device=env.device), is_relative=True + ) # Create navigation control configuration config = LocomanipulationSDGControlConfig( @@ -650,30 +892,27 @@ def replay( approach_distance=approach_distance, ) - # Set up navigation scene and path planning - occupancy_map, base_path_helper, base_goal, base_goal_approach = setup_navigation_scene( - env, input_episode_data, approach_distance, randomize_placement + nav_scene = setup_navigation_scene( + env, input_episode_data, approach_distance, randomize_placement, draw_visualization ) + if nav_scene is None: + print("Failed to setup navigation scene", flush=True) + return False - # Visualize occupancy map and path if requested - if draw_visualization: - occupancy_map_add_to_stage( - occupancy_map, - stage=omni.usd.get_context().get_stage(), - path="/OccupancyMap", - z_offset=0.01, - draw_path=base_path_helper.points, - ) + if sensor_camera_view: + _set_sensor_camera_view() # Initialize state machine output_data = LocomanipulationSDGOutputData() current_state = LocomanipulationSDGDataGenerationState.GRASP_OBJECT + previous_state = None recording_step = 0 # Main simulation loop with state machine while simulation_app.is_running() and not simulation_app.is_exiting(): - - print(f"Current state: {current_state.name}, Recording step: {recording_step}") + if current_state != previous_state: + print(f"State changed: {current_state.name}, Recording step: {recording_step}", flush=True) + previous_state = current_state # Execute state-specific logic using helper functions if current_state == LocomanipulationSDGDataGenerationState.GRASP_OBJECT: @@ -688,24 +927,32 @@ def replay( elif current_state == LocomanipulationSDGDataGenerationState.NAVIGATE: current_state = handle_navigate_state( - env, input_episode_data, recording_step, base_path_helper, base_goal_approach, config, output_data + env, + input_episode_data, + recording_step, + nav_scene.base_path_helper, + nav_scene.base_goal_approach, + config, + output_data, ) elif current_state == LocomanipulationSDGDataGenerationState.APPROACH: current_state = handle_approach_state( - env, input_episode_data, recording_step, base_goal, config, output_data + env, input_episode_data, recording_step, nav_scene.base_goal, config, output_data ) elif current_state == LocomanipulationSDGDataGenerationState.DROP_OFF_OBJECT: recording_step, next_state = handle_drop_off_state( - env, input_episode_data, recording_step, base_goal, config, output_data + env, input_episode_data, recording_step, nav_scene.base_goal, config, output_data ) if next_state is None: # End of episode data break current_state = next_state # Populate additional output data fields - populate_output_data(env, output_data, base_goal, base_goal_approach, base_path_helper.points) + populate_output_data( + env, output_data, nav_scene.base_goal, nav_scene.base_goal_approach, nav_scene.base_path_helper.points + ) # Attach output data to environment for recording env._locomanipulation_sdg_output_data = output_data @@ -718,33 +965,59 @@ def replay( left_hand_pose_target=output_data.left_hand_pose_target, right_hand_pose_target=output_data.right_hand_pose_target, ) + _, _, reset_terminated, reset_time_outs, _ = env.step(action) - env.step(action) + if reset_terminated[0] or reset_time_outs[0]: + print(f"Environment terminated at state {current_state.name}, step {recording_step}", flush=True) + success_terminated = False + # Check if termination was due to success + if hasattr(env, "termination_manager") and "success" in env.termination_manager.active_terms: + success_terminated = bool(env.termination_manager.get_term("success")[0].item()) -if __name__ == "__main__": + if success_terminated: + print(f"Success termination at state {current_state.name}, step {recording_step}", flush=True) + else: + print(f"Non-success termination at state {current_state.name}, step {recording_step}", flush=True) - with torch.no_grad(): + return success_terminated + print("Replay completed!", flush=True) + return False + + +if __name__ == "__main__": + with torch.no_grad(): # Create environment if args_cli.task is not None: env_name = args_cli.task.split(":")[-1] if env_name is None: raise ValueError("Task/env name was not specified nor found in the dataset.") - env_cfg = parse_env_cfg(env_name, device=args_cli.device, num_envs=1) + env_cfg = parse_env_cfg(env_name, device=args_cli.device, num_envs=1, overrides=hydra_overrides) env_cfg.sim.device = "cpu" env_cfg.recorders.dataset_export_dir_path = os.path.dirname(args_cli.output_file) env_cfg.recorders.dataset_filename = os.path.basename(args_cli.output_file) + env_cfg.recorders.dataset_export_mode = DatasetExportMode.EXPORT_SUCCEEDED_ONLY + env_cfg.recorders.export_in_record_pre_reset = True + + if args_cli.background_usd_path is not None and args_cli.background_occupancy_yaml_file is not None: + env_cfg.background_usd_path = args_cli.background_usd_path + env_cfg.background_occupancy_yaml_file = args_cli.background_occupancy_yaml_file + + env_cfg.high_res_video = args_cli.high_res_video env = gym.make(args_cli.task, cfg=env_cfg).unwrapped # Load input data input_dataset_file_handler = HDF5DatasetFileHandler() input_dataset_file_handler.open(args_cli.dataset) + is_legacy_quat_format = input_dataset_file_handler.is_legacy_quaternion_format() - for i in range(args_cli.num_runs): + run_id = 0 + success_runs = 0 + while success_runs < args_cli.num_runs: if args_cli.demo is None: demo = random.choice(list(input_dataset_file_handler.get_episode_names())) else: @@ -752,7 +1025,17 @@ def replay( input_episode_data = input_dataset_file_handler.load_episode(demo, args_cli.device) - replay( + assert input_episode_data is not None + assert "actions" in input_episode_data.data + + # See G1LocomanipulationSDGEnv.build_action_vector() for the action layout; + # left_quat=[3:7], right_quat=[10:14]. + if is_legacy_quat_format: + actions = input_episode_data.data["actions"] + actions[:, 3:7] = convert_quat(actions[:, 3:7], to="xyzw") # left hand quat + actions[:, 10:14] = convert_quat(actions[:, 10:14], to="xyzw") # right hand quat + + success = replay( env=env, input_episode_data=input_episode_data, lift_step=args_cli.lift_step, @@ -766,9 +1049,20 @@ def replay( angle_threshold=args_cli.angle_threshold, approach_distance=args_cli.approach_distance, randomize_placement=args_cli.randomize_placement, + sensor_camera_view=args_cli.sensor_camera_view, ) - env.reset() # FIXME: hack to handle missing final recording + run_id += 1 + + if success: + success_runs += 1 + print("successful episodes count", env.recorder_manager.exported_successful_episode_count, flush=True) + else: + print(f"Run {run_id} failed (demo={demo}), retrying...", flush=True) + + env.recorder_manager.close() + if getattr(env, "viewport_camera_controller", None) is not None: + env.viewport_camera_controller.update_view_to_world() env.close() simulation_app.close() diff --git a/scripts/imitation_learning/locomanipulation_sdg/plot_navigation_trajectory.py b/scripts/imitation_learning/locomanipulation_sdg/plot_navigation_trajectory.py index 706b2d65..4022ea75 100644 --- a/scripts/imitation_learning/locomanipulation_sdg/plot_navigation_trajectory.py +++ b/scripts/imitation_learning/locomanipulation_sdg/plot_navigation_trajectory.py @@ -1,28 +1,27 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -"""Script to visualize navigation datasets. +"""Script to visualize navigation datasets from locomanipulation SDG. -Loads a navigation dataset and generates plots showing paths, poses and obstacles. - -Args: - dataset: Path to the HDF5 dataset file containing recorded demonstrations. - output_dir: Directory path where visualization plots will be saved. - figure_size: Size of the generated figures (width, height). - demo_filter: If provided, only visualize specific demo(s). Can be a single demo name or comma-separated list. +Loads an HDF5 dataset containing locomanipulation_sdg_output_data and generates +plots showing base path, base pose, object pose, start/end fixtures, and obstacles. """ import argparse +import os + import h5py import matplotlib.pyplot as plt -import os -def main(): - """Main function to process dataset and generate visualizations.""" - # add argparse arguments +def main() -> None: + """Load dataset, filter demos if requested, and save navigation visualization plots. + + Reads HDF5 from --input_file, optionally restricts to --demo_filter, and writes + one PNG per demo to --output_dir with path, poses, and obstacle positions. + """ parser = argparse.ArgumentParser( description="Visualize navigation dataset from locomanipulation sdg demonstrations." ) @@ -44,22 +43,17 @@ def main(): help="If provided, only visualize specific demo(s). Can be a single demo name or comma-separated list.", ) - # parse the arguments args = parser.parse_args() - # Validate inputs if not os.path.exists(args.input_file): raise FileNotFoundError(f"Dataset file not found: {args.input_file}") - # Create output directory if it doesn't exist os.makedirs(args.output_dir, exist_ok=True) - # Load dataset dataset = h5py.File(args.input_file, "r") demos = list(dataset["data"].keys()) - # Filter demos if specified if args.demo_filter: filter_demos = [d.strip() for d in args.demo_filter.split(",")] demos = [d for d in demos if d in filter_demos] @@ -86,7 +80,6 @@ def main(): plt.plot(object_pose[:, 0], object_pose[:, 1], "b--", label="Object Pose", linewidth=2) plt.plot(obstacle_poses[0, :, 0], obstacle_poses[0, :, 1], "ro", label="Obstacles", markersize=8) - # Add start and end markers plt.plot(start_pose[0, 0], start_pose[0, 1], "gs", label="Start", markersize=12) plt.plot(end_pose[0, 0], end_pose[0, 1], "rs", label="End", markersize=12) diff --git a/scripts/imitation_learning/robomimic/play.py b/scripts/imitation_learning/robomimic/play.py index 77b7d2e1..1f381bb6 100644 --- a/scripts/imitation_learning/robomimic/play.py +++ b/scripts/imitation_learning/robomimic/play.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -22,7 +22,9 @@ import argparse -from isaaclab.app import AppLauncher +from isaaclab.app import AppLauncher, scan + +from isaaclab_tasks.utils import resolve_task_config # add argparse arguments parser = argparse.ArgumentParser(description="Evaluate robomimic policy for Isaac Lab environment.") @@ -40,37 +42,34 @@ parser.add_argument( "--norm_factor_max", type=float, default=None, help="Optional: maximum value of the normalization factor." ) -parser.add_argument("--enable_pinocchio", default=False, action="store_true", help="Enable Pinocchio.") - # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) -# parse the arguments -args_cli = parser.parse_args() - -if args_cli.enable_pinocchio: - # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim - # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter - import pinocchio # noqa: F401 +# parse the arguments, forwarding unrecognized ones as Hydra-style task config overrides +args_cli, hydra_overrides = parser.parse_known_args() # launch omniverse app -app_launcher = AppLauncher(args_cli) +# Only enable rendering for tasks that actually declare Kit camera sensors: this script also +# plays policies trained on low-dimensional observations, which should not pay for the RTX +# renderer. ``resolve_task_config`` is safe to call before Kit is launched, and ``scan`` is the +# same detection ``launch_simulation`` uses, so this matches how camera enabling is resolved +# elsewhere now that the ``--enable_cameras`` flag is gone. +# ``overrides`` must be passed explicitly: this script keeps its own flags in ``sys.argv`` rather +# than stripping them, so letting Hydra fall back to reading ``sys.argv`` makes it reject them. +env_cfg_for_scan, _ = resolve_task_config(args_cli.task, "", overrides=hydra_overrides) +app_launcher = AppLauncher(args_cli, enable_cameras=scan(env_cfg_for_scan, args_cli).has_kit_camera) simulation_app = app_launcher.app """Rest everything follows.""" import copy -import gymnasium as gym -import numpy as np import random -import torch +import gymnasium as gym +import numpy as np import robomimic.utils.file_utils as FileUtils import robomimic.utils.torch_utils as TorchUtils - -if args_cli.enable_pinocchio: - import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 - import isaaclab_tasks.manager_based.locomanipulation.pick_place # noqa: F401 +import torch from isaaclab_tasks.utils import parse_env_cfg @@ -143,7 +142,13 @@ def rollout(policy, env, success_term, horizon, device): def main(): """Run a trained policy from robomimic with Isaac Lab environment.""" # parse configuration - env_cfg = parse_env_cfg(args_cli.task, device=args_cli.device, num_envs=1, use_fabric=not args_cli.disable_fabric) + env_cfg = parse_env_cfg( + args_cli.task, + device=args_cli.device, + num_envs=1, + use_fabric=not args_cli.disable_fabric, + overrides=hydra_overrides, + ) # Set observations to dictionary mode for Robomimic env_cfg.observations.policy.concatenate_terms = False @@ -170,14 +175,15 @@ def main(): # Acquire device device = TorchUtils.get_torch_device(try_to_use_cuda=True) - # Run policy - results = [] - for trial in range(args_cli.num_rollouts): - print(f"[INFO] Starting trial {trial}") - policy, _ = FileUtils.policy_from_checkpoint(ckpt_path=args_cli.checkpoint, device=device) - terminated, traj = rollout(policy, env, success_term, args_cli.horizon, device) - results.append(terminated) - print(f"[INFO] Trial {trial}: {terminated}\n") + with torch.inference_mode(): + # Run policy + results = [] + for trial in range(args_cli.num_rollouts): + print(f"[INFO] Starting trial {trial}") + policy, _ = FileUtils.policy_from_checkpoint(ckpt_path=args_cli.checkpoint, device=device) + terminated, traj = rollout(policy, env, success_term, args_cli.horizon, device) + results.append(terminated) + print(f"[INFO] Trial {trial}: {terminated}\n") print(f"\nSuccessful trials: {results.count(True)}, out of {len(results)} trials") print(f"Success rate: {results.count(True) / len(results)}") diff --git a/scripts/imitation_learning/robomimic/robust_eval.py b/scripts/imitation_learning/robomimic/robust_eval.py index 1f93d413..32a87554 100644 --- a/scripts/imitation_learning/robomimic/robust_eval.py +++ b/scripts/imitation_learning/robomimic/robust_eval.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -28,7 +28,9 @@ import argparse -from isaaclab.app import AppLauncher +from isaaclab.app import AppLauncher, scan + +from isaaclab_tasks.utils import resolve_task_config # add argparse arguments parser = argparse.ArgumentParser(description="Evaluate robomimic policy for Isaac Lab environment.") @@ -57,33 +59,35 @@ parser.add_argument( "--norm_factor_max", type=float, default=None, help="Optional: maximum value of the normalization factor." ) -parser.add_argument("--enable_pinocchio", default=False, action="store_true", help="Enable Pinocchio.") # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) -# parse the arguments -args_cli = parser.parse_args() - -if args_cli.enable_pinocchio: - # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim - # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter - import pinocchio # noqa: F401 +# parse the arguments, forwarding unrecognized ones as Hydra-style task config overrides +args_cli, hydra_overrides = parser.parse_known_args() # launch omniverse app -app_launcher = AppLauncher(args_cli) +# Only enable rendering for tasks that actually declare Kit camera sensors: this script also +# evaluates policies trained on low-dimensional observations, which should not pay for the RTX +# renderer. ``resolve_task_config`` is safe to call before Kit is launched, and ``scan`` is the +# same detection ``launch_simulation`` uses, so this matches how camera enabling is resolved +# elsewhere now that the ``--enable_cameras`` flag is gone. +# ``overrides`` must be passed explicitly: this script keeps its own flags in ``sys.argv`` rather +# than stripping them, so letting Hydra fall back to reading ``sys.argv`` makes it reject them. +env_cfg_for_scan, _ = resolve_task_config(args_cli.task, "", overrides=hydra_overrides) +app_launcher = AppLauncher(args_cli, enable_cameras=scan(env_cfg_for_scan, args_cli).has_kit_camera) simulation_app = app_launcher.app """Rest everything follows.""" import copy -import gymnasium as gym import os import pathlib import random -import torch +import gymnasium as gym import robomimic.utils.file_utils as FileUtils import robomimic.utils.torch_utils as TorchUtils +import torch from isaaclab_tasks.utils import parse_env_cfg @@ -219,7 +223,13 @@ def evaluate_model( def main() -> None: """Run evaluation of trained policies from robomimic with Isaac Lab environment.""" # Parse configuration - env_cfg = parse_env_cfg(args_cli.task, device=args_cli.device, num_envs=1, use_fabric=not args_cli.disable_fabric) + env_cfg = parse_env_cfg( + args_cli.task, + device=args_cli.device, + num_envs=1, + use_fabric=not args_cli.disable_fabric, + overrides=hydra_overrides, + ) # Set observations to dictionary mode for Robomimic env_cfg.observations.policy.concatenate_terms = False diff --git a/scripts/imitation_learning/robomimic/train.py b/scripts/imitation_learning/robomimic/train.py index 837a8972..4369f594 100644 --- a/scripts/imitation_learning/robomimic/train.py +++ b/scripts/imitation_learning/robomimic/train.py @@ -1,7 +1,26 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause + +# MIT License +# +# Copyright (c) 2021 Stanford Vision and Learning Lab +# +# 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. @@ -34,41 +53,35 @@ """Rest everything follows.""" -# Standard library imports import argparse - -# Third-party imports -import gymnasium as gym -import h5py import importlib import json -import numpy as np import os import shutil import sys import time -import torch import traceback from collections import OrderedDict -from torch.utils.data import DataLoader +import gymnasium as gym +import h5py +import numpy as np import psutil - -# Robomimic imports import robomimic.utils.env_utils as EnvUtils import robomimic.utils.file_utils as FileUtils import robomimic.utils.obs_utils as ObsUtils import robomimic.utils.torch_utils as TorchUtils import robomimic.utils.train_utils as TrainUtils +import torch from robomimic.algo import algo_factory from robomimic.config import Config, config_factory from robomimic.utils.log_utils import DataLogger, PrintLogger +from torch.utils.data import DataLoader -# Isaac Lab imports (needed so that environment is registered) import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -import isaaclab_tasks.manager_based.locomanipulation.pick_place # noqa: F401 -import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 +import isaaclab_tasks.contrib.locomanip_pick_place # noqa: F401 +import isaaclab_tasks.contrib.pick_place # noqa: F401 def normalize_hdf5_actions(config: Config, log_dir: str) -> str: diff --git a/scripts/reinforcement_learning/play.py b/scripts/reinforcement_learning/play.py new file mode 100644 index 00000000..0ca047f3 --- /dev/null +++ b/scripts/reinforcement_learning/play.py @@ -0,0 +1,25 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Unified playback executable for Isaac Lab reinforcement learning workflows.""" + +# Warp captures ``enable_backward`` when a module is created, which happens at import +# time, so it has to be set before importing anything that defines Warp kernels. +# Isaac Lab does not use Warp autodiff; skipping adjoint codegen roughly halves the +# time spent building kernels on a cold kernel cache. +import warp as wp + +wp.config.enable_backward = False + +from isaaclab_rl.entrypoints import run_play_cli # noqa: E402 + + +def main(argv: list[str] | None = None) -> int: + """Run the selected reinforcement learning play library.""" + return run_play_cli(argv) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/reinforcement_learning/ray/grok_cluster_with_kubectl.py b/scripts/reinforcement_learning/ray/grok_cluster_with_kubectl.py index 11c1aa3f..b7b3c5cf 100644 --- a/scripts/reinforcement_learning/ray/grok_cluster_with_kubectl.py +++ b/scripts/reinforcement_learning/ray/grok_cluster_with_kubectl.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -164,11 +164,12 @@ def process_cluster(cluster_info: dict, ray_head_name: str = "head") -> str: For each cluster, check that it is running, and get the Ray head address that will accept jobs. Args: - cluster_info (dict): A dictionary containing cluster information with keys 'cluster', 'pods', and 'namespace'. - ray_head_name (str, optional): The name of the ray head container. Defaults to "head". + cluster_info: A dictionary containing cluster information with keys 'cluster', 'pods', and 'namespace'. + ray_head_name: The name of the ray head container. Defaults to "head". Returns: - str: A string containing the cluster name and its Ray head address, or an error message if the head pod or Ray address is not found. + A string containing the cluster name and its Ray head address, or an error message if + the head pod or Ray address is not found. """ cluster, pods, namespace = cluster_info head_pod = None diff --git a/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cartpole_cfg.py b/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cartpole_cfg.py index 6e8d9a08..20318d35 100644 --- a/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cartpole_cfg.py +++ b/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cartpole_cfg.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import pathlib @@ -13,44 +13,62 @@ import util import vision_cfg from ray import tune +from ray.tune.progress_reporter import CLIReporter from ray.tune.stopper import Stopper class CartpoleRGBNoTuneJobCfg(vision_cfg.CameraJobCfg): def __init__(self, cfg: dict = {}): cfg = util.populate_isaac_ray_cfg_args(cfg) - cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-RGB-v0"]) + cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-Camera"]) super().__init__(cfg, vary_env_count=False, vary_cnn=False, vary_mlp=False) class CartpoleRGBCNNOnlyJobCfg(vision_cfg.CameraJobCfg): def __init__(self, cfg: dict = {}): cfg = util.populate_isaac_ray_cfg_args(cfg) - cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-RGB-v0"]) + cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-Camera"]) super().__init__(cfg, vary_env_count=False, vary_cnn=True, vary_mlp=False) class CartpoleRGBJobCfg(vision_cfg.CameraJobCfg): def __init__(self, cfg: dict = {}): cfg = util.populate_isaac_ray_cfg_args(cfg) - cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-RGB-v0"]) + cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-Camera"]) super().__init__(cfg, vary_env_count=True, vary_cnn=True, vary_mlp=True) class CartpoleResNetJobCfg(vision_cfg.ResNetCameraJob): def __init__(self, cfg: dict = {}): cfg = util.populate_isaac_ray_cfg_args(cfg) - cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-RGB-ResNet18-v0"]) + cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-Camera"]) + cfg["hydra_args"]["presets"] = "resnet18" super().__init__(cfg) class CartpoleTheiaJobCfg(vision_cfg.TheiaCameraJob): def __init__(self, cfg: dict = {}): cfg = util.populate_isaac_ray_cfg_args(cfg) - cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-RGB-TheiaTiny-v0"]) + cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-Camera"]) + cfg["hydra_args"]["presets"] = "theia_tiny" super().__init__(cfg) +class CustomCartpoleProgressReporter(CLIReporter): + def __init__(self): + super().__init__( + metric_columns={ + "training_iteration": "iter", + "time_total_s": "total time (s)", + "Episode/Episode_Reward/alive": "alive", + "Episode/Episode_Reward/cart_vel": "cart velocity", + "rewards/time": "rewards/time", + }, + max_report_frequency=5, + sort_by_metric=True, + ) + + class CartpoleEarlyStopper(Stopper): def __init__(self): self._bad_trials = set() @@ -60,7 +78,7 @@ def __call__(self, trial_id: str, result: dict[str, Any]) -> bool: out_of_bounds = result.get("Episode/Episode_Termination/cart_out_of_bounds") # Mark the trial for stopping if conditions are met - if 20 <= iter and out_of_bounds is not None and out_of_bounds > 0.85: + if iter >= 20 and out_of_bounds is not None and out_of_bounds > 0.85: self._bad_trials.add(trial_id) return trial_id in self._bad_trials diff --git a/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cfg.py b/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cfg.py index b6665538..31a5d55f 100644 --- a/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cfg.py +++ b/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cfg.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -30,10 +30,11 @@ def _get_batch_size_divisors(batch_size: int, min_size: int = 128) -> list[int]: def __init__(self, cfg={}, vary_env_count: bool = False, vary_cnn: bool = False, vary_mlp: bool = False): cfg = util.populate_isaac_ray_cfg_args(cfg) + library = cfg["runner_args"].setdefault("--rl_library", "rl_games") + if library != "rl_games": + raise ValueError("Camera tuning overrides require --rl_library rl_games.") # Basic configuration - cfg["runner_args"]["headless_singleton"] = "--headless" - cfg["runner_args"]["enable_cameras_singleton"] = "--enable_cameras" cfg["hydra_args"]["agent.params.config.max_epochs"] = 200 if vary_env_count: # Vary the env count, and horizon length, and select a compatible mini-batch size @@ -85,12 +86,14 @@ def get_cnn_layers(_): if next_size <= 0: break - layers.append({ - "filters": tune.randint(16, 32).sample(), - "kernel_size": str(kernel), - "strides": str(stride), - "padding": str(padding), - }) + layers.append( + { + "filters": tune.randint(16, 32).sample(), + "kernel_size": str(kernel), + "strides": str(stride), + "padding": str(padding), + } + ) size = next_size return layers @@ -98,7 +101,6 @@ def get_cnn_layers(_): cfg["hydra_args"]["agent.params.network.cnn.convs"] = tune.sample_from(get_cnn_layers) if vary_mlp: # Vary the MLP structure; neurons (units) per layer, number of layers, - max_num_layers = 6 max_neurons_per_layer = 128 if "env.observations.policy.image.params.model_name" in cfg["hydra_args"]: @@ -140,12 +142,14 @@ class TheiaCameraJob(CameraJobCfg): def __init__(self, cfg: dict = {}): cfg = util.populate_isaac_ray_cfg_args(cfg) - cfg["hydra_args"]["env.observations.policy.image.params.model_name"] = tune.choice([ - "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", - ]) + cfg["hydra_args"]["env.observations.policy.image.params.model_name"] = tune.choice( + [ + "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", + ] + ) super().__init__(cfg, vary_env_count=True, vary_cnn=False, vary_mlp=True) diff --git a/scripts/reinforcement_learning/ray/launch.py b/scripts/reinforcement_learning/ray/launch.py index 43196d47..3a3be716 100644 --- a/scripts/reinforcement_learning/ray/launch.py +++ b/scripts/reinforcement_learning/ray/launch.py @@ -1,17 +1,8 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -import argparse -import pathlib -import subprocess -import yaml - -import util -from jinja2 import Environment, FileSystemLoader -from kubernetes import config - """This script helps create one or more KubeRay clusters. Usage: @@ -34,6 +25,18 @@ --num_workers 1 2 --num_clusters 1 \ --worker_accelerator nvidia-l4 nvidia-tesla-t4 --gpu_per_worker 1 2 4 """ + +import argparse +import pathlib +import subprocess + +import yaml +from jinja2 import Environment, FileSystemLoader +from kubernetes import config + +# Local imports +import util # isort: skip + RAY_DIR = pathlib.Path(__file__).parent diff --git a/scripts/reinforcement_learning/ray/mlflow_to_local_tensorboard.py b/scripts/reinforcement_learning/ray/mlflow_to_local_tensorboard.py index 4c932e32..2c45f1cd 100644 --- a/scripts/reinforcement_learning/ray/mlflow_to_local_tensorboard.py +++ b/scripts/reinforcement_learning/ray/mlflow_to_local_tensorboard.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,10 +9,10 @@ import os import sys from concurrent.futures import ProcessPoolExecutor, as_completed -from torch.utils.tensorboard import SummaryWriter import mlflow from mlflow.tracking import MlflowClient +from torch.utils.tensorboard import SummaryWriter def setup_logging(level=logging.INFO): diff --git a/scripts/reinforcement_learning/ray/submit_job.py b/scripts/reinforcement_learning/ray/submit_job.py index 75b8b534..21fb6a3d 100644 --- a/scripts/reinforcement_learning/ray/submit_job.py +++ b/scripts/reinforcement_learning/ray/submit_job.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -46,7 +46,8 @@ python3 scripts/reinforcement_learning/ray/submit_job.py --aggregate_jobs wrap_resources.py --test # Example: submitting tasks with specific resources, and supporting pip packages and py_modules - # You may use relative paths for task_cfg and py_modules, placing them in the scripts/reinforcement_learning/ray directory, which will be uploaded to the cluster. + # You may use relative paths for task_cfg and py_modules, placing them in the + # "scripts/reinforcement_learning/ray" directory, which will be uploaded to the cluster. python3 scripts/reinforcement_learning/ray/submit_job.py --aggregate_jobs task_runner.py --task_cfg tasks.yaml # For all command line arguments diff --git a/scripts/reinforcement_learning/ray/task_runner.py b/scripts/reinforcement_learning/ray/task_runner.py index 6ac3ef51..7b10237f 100644 --- a/scripts/reinforcement_learning/ray/task_runner.py +++ b/scripts/reinforcement_learning/ray/task_runner.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -22,8 +22,13 @@ - `pip`: List of extra pip packages to install before running any tasks. - `py_modules`: List of additional Python module paths (directories or files) to include in the runtime environment. - `concurrent`: (bool) It determines task dispatch semantics: - - If `concurrent: true`, **all tasks are scheduled as a batch**. The script waits until sufficient resources are available for every task in the batch, then launches all tasks together. If resources are insufficient, all tasks remain blocked until the cluster can support the full batch. - - If `concurrent: false`, tasks are launched as soon as resources are available for each individual task, and Ray independently schedules them. This may result in non-simultaneous task start times. + - If `concurrent: true`, **all tasks are scheduled as a batch**. The script waits until + sufficient resources are available for every task in the batch, then launches all tasks + together. If resources are insufficient, all tasks remain blocked until the cluster can + support the full batch. + - If `concurrent: false`, tasks are launched as soon as resources are available for each + individual task, and Ray independently schedules them. This may result in non-simultaneous + task start times. - `tasks`: List of task specifications, each with: - `name`: String identifier for the task. - `py_args`: Arguments to the Python interpreter (e.g., script/module, flags, user arguments). @@ -33,14 +38,16 @@ - `node` (optional): Node placement constraints. - `specific` (str): Type of node placement, support `hostname`, `node_id`, or `any`. - `any`: Place the task on any available node. - - `hostname`: Place the task on a specific hostname. `hostname` must be specified in the node field. - - `node_id`: Place the task on a specific node ID. `node_id` must be specified in the node field. + - `hostname`: Place the task on a specific hostname. `hostname` must be specified + in the node field. + - `node_id`: Place the task on a specific node ID. `node_id` must be specified in + the node field. - `hostname` (str): Specific hostname to place the task on. - `node_id` (str): Specific node ID to place the task on. Typical usage: ---------------- +-------------- .. code-block:: bash @@ -51,15 +58,16 @@ python task_runner.py --task_cfg /path/to/tasks.yaml YAML configuration example-1: ---------------------------- +----------------------------- + .. code-block:: yaml pip: ["xxx"] py_modules: ["my_package/my_package"] concurrent: false tasks: - - name: "Isaac-Cartpole-v0" - py_args: "-m torch.distributed.run --nnodes=1 --nproc_per_node=2 --rdzv_endpoint=localhost:29501 /workspace/isaaclab/scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Cartpole-v0 --max_iterations 200 --headless --distributed" + - name: "Isaac-Cartpole" + py_args: "-m torch.distributed.run --nnodes=1 --nproc_per_node=2 --rdzv_endpoint=localhost:29501 /workspace/isaaclab/scripts/reinforcement_learning/train.py --rl_library rsl_rl --task=Isaac-Cartpole --max_iterations 200 --distributed" num_gpus: 2 num_cpus: 10 memory: 10737418240 @@ -70,23 +78,24 @@ memory: 10*1024*1024*1024 YAML configuration example-2: ---------------------------- +----------------------------- + .. code-block:: yaml pip: ["xxx"] py_modules: ["my_package/my_package"] concurrent: true tasks: - - name: "Isaac-Cartpole-v0-multi-node-train-1" - py_args: "-m torch.distributed.run --nproc_per_node=1 --nnodes=2 --node_rank=0 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=localhost:5555 /workspace/isaaclab/scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Cartpole-v0 --headless --distributed --max_iterations 1000" + - name: "Isaac-Cartpole-multi-node-train-1" + py_args: "-m torch.distributed.run --nproc_per_node=1 --nnodes=2 --node_rank=0 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=localhost:5555 /workspace/isaaclab/scripts/reinforcement_learning/train.py --rl_library rsl_rl --task=Isaac-Cartpole --distributed --max_iterations 1000" num_gpus: 1 num_cpus: 10 memory: 10*1024*1024*1024 node: specific: "hostname" hostname: "xxx" - - name: "Isaac-Cartpole-v0-multi-node-train-2" - py_args: "-m torch.distributed.run --nproc_per_node=1 --nnodes=2 --node_rank=1 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=x.x.x.x:5555 /workspace/isaaclab/scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Cartpole-v0 --headless --distributed --max_iterations 1000" + - name: "Isaac-Cartpole-multi-node-train-2" + py_args: "-m torch.distributed.run --nproc_per_node=1 --nnodes=2 --node_rank=1 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=x.x.x.x:5555 /workspace/isaaclab/scripts/reinforcement_learning/train.py --rl_library rsl_rl --task=Isaac-Cartpole --distributed --max_iterations 1000" num_gpus: 1 num_cpus: 10 memory: 10*1024*1024*1024 @@ -95,13 +104,69 @@ hostname: "xxx" To stop all tasks early, press Ctrl+C; the script will cancel all running Ray tasks. -""" +""" # noqa: E501 import argparse -import yaml +import ast +import operator from datetime import datetime -import util +import yaml + +# Local imports +import util # isort: skip + +# Safe operators for arithmetic expression evaluation +_SAFE_OPERATORS = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: operator.mul, + ast.Div: operator.truediv, + ast.FloorDiv: operator.floordiv, + ast.Pow: operator.pow, + ast.Mod: operator.mod, + ast.USub: operator.neg, + ast.UAdd: operator.pos, +} + + +def safe_eval_arithmetic(expr: str) -> int | float: + """ + Safely evaluate a string containing only arithmetic expressions. + + Supports: +, -, *, /, //, **, % and numeric literals. + Raises ValueError for any non-arithmetic expressions. + + Args: + expr: A string containing an arithmetic expression (e.g., "10*1024*1024"). + + Returns: + The numeric result of the expression. + + Raises: + ValueError: If the expression contains non-arithmetic operations. + """ + + def _eval_node(node: ast.AST) -> int | float: + if isinstance(node, ast.Expression): + return _eval_node(node.body) + elif isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): + return node.value + elif isinstance(node, ast.BinOp) and type(node.op) in _SAFE_OPERATORS: + left = _eval_node(node.left) + right = _eval_node(node.right) + return _SAFE_OPERATORS[type(node.op)](left, right) + elif isinstance(node, ast.UnaryOp) and type(node.op) in _SAFE_OPERATORS: + operand = _eval_node(node.operand) + return _SAFE_OPERATORS[type(node.op)](operand) + else: + raise ValueError(f"Unsafe expression: {ast.dump(node)}") + + try: + tree = ast.parse(expr.strip(), mode="eval") + return _eval_node(tree) + except (SyntaxError, TypeError) as e: + raise ValueError(f"Invalid arithmetic expression: {expr}") from e def parse_args() -> argparse.Namespace: @@ -109,7 +174,7 @@ def parse_args() -> argparse.Namespace: Parse command-line arguments for the Ray task runner. Returns: - argparse.Namespace: The namespace containing parsed CLI arguments: + A namespace containing parsed CLI arguments: - task_cfg (str): Path to the YAML task file. - ray_address (str): Ray cluster address. - test (bool): Whether to run a GPU resource isolation sanity check. @@ -143,11 +208,14 @@ def parse_task_resource(task: dict) -> util.JobResource: """ resource = util.JobResource() if "num_gpus" in task: - resource.num_gpus = eval(task["num_gpus"]) if isinstance(task["num_gpus"], str) else task["num_gpus"] + value = task["num_gpus"] + resource.num_gpus = safe_eval_arithmetic(value) if isinstance(value, str) else value if "num_cpus" in task: - resource.num_cpus = eval(task["num_cpus"]) if isinstance(task["num_cpus"], str) else task["num_cpus"] + value = task["num_cpus"] + resource.num_cpus = safe_eval_arithmetic(value) if isinstance(value, str) else value if "memory" in task: - resource.memory = eval(task["memory"]) if isinstance(task["memory"], str) else task["memory"] + value = task["memory"] + resource.memory = safe_eval_arithmetic(value) if isinstance(value, str) else value return resource diff --git a/scripts/reinforcement_learning/ray/tuner.py b/scripts/reinforcement_learning/ray/tuner.py index 908aca1b..92357cf4 100644 --- a/scripts/reinforcement_learning/ray/tuner.py +++ b/scripts/reinforcement_learning/ray/tuner.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import argparse @@ -14,6 +14,7 @@ import util from ray import air, tune from ray.tune import Callback +from ray.tune.progress_reporter import ProgressReporter from ray.tune.search.optuna import OptunaSearch from ray.tune.search.repeater import Repeater from ray.tune.stopper import CombinedStopper @@ -41,15 +42,20 @@ .. code-block:: bash - ./isaaclab.sh -p scripts/reinforcement_learning/ray/tuner.py -h + uv run python scripts/reinforcement_learning/ray/tuner.py -h # Examples # Local - ./isaaclab.sh -p scripts/reinforcement_learning/ray/tuner.py --run_mode local \ + uv run python scripts/reinforcement_learning/ray/tuner.py --run_mode local \ --cfg_file scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cartpole_cfg.py \ --cfg_class CartpoleTheiaJobCfg + # Local with a custom progress reporter + uv run python scripts/reinforcement_learning/ray/tuner.py \ + --cfg_file scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cartpole_cfg.py \ + --cfg_class CartpoleTheiaJobCfg \ + --progress_reporter CustomCartpoleProgressReporter # Remote (run grok cluster or create config file mentioned in :file:`submit_job.py`) - ./isaaclab.sh -p scripts/reinforcement_learning/ray/submit_job.py \ + uv run python scripts/reinforcement_learning/ray/submit_job.py \ --aggregate_jobs tuner.py \ --cfg_file hyperparameter_tuning/vision_cartpole_cfg.py \ --cfg_class CartpoleTheiaJobCfg --mlflow_uri @@ -59,7 +65,7 @@ DOCKER_PREFIX = "/workspace/isaaclab/" BASE_DIR = os.path.expanduser("~") PYTHON_EXEC = "./isaaclab.sh -p" -WORKFLOW = "scripts/reinforcement_learning/rl_games/train.py" +WORKFLOW = "scripts/reinforcement_learning/train.py" NUM_WORKERS_PER_NODE = 1 # needed for local parallelism PROCESS_RESPONSE_TIMEOUT = 200.0 # seconds to wait before killing the process when it stops responding MAX_LINES_TO_SEARCH_EXPERIMENT_LOGS = 1000 # maximum number of lines to read from the training process logs @@ -229,6 +235,7 @@ def _cleanup_trial(self, trial): def invoke_tuning_run( cfg: dict, args: argparse.Namespace, + progress_reporter: ProgressReporter | None = None, stopper: tune.Stopper | None = None, ) -> None: """Invoke an Isaac-Ray tuning run. @@ -237,6 +244,7 @@ def invoke_tuning_run( Args: cfg: Configuration dictionary extracted from job setup args: Command-line arguments related to tuning. + progress_reporter: Custom progress reporter. Defaults to CLIReporter or JupyterNotebookReporter if not provided. stopper: Custom stopper, optional. """ # Allow for early exit @@ -266,10 +274,23 @@ def invoke_tuning_run( repeat_search = Repeater(searcher, repeat=args.repeat_run_count) # Configure the stoppers - stoppers: CombinedStopper = CombinedStopper(*[ - LogExtractionErrorStopper(max_errors=MAX_LOG_EXTRACTION_ERRORS), - *([stopper] if stopper is not None else []), - ]) + stoppers: CombinedStopper = CombinedStopper( + *[ + LogExtractionErrorStopper(max_errors=MAX_LOG_EXTRACTION_ERRORS), + *([stopper] if stopper is not None else []), + ] + ) + + if progress_reporter is not None: + os.environ["RAY_AIR_NEW_OUTPUT"] = "0" + if ( + getattr(progress_reporter, "_metric", None) is not None + or getattr(progress_reporter, "_mode", None) is not None + ): + raise ValueError( + "Do not set or directly in the custom progress reporter class, " + "provide them as arguments to tuner.py instead." + ) if args.run_mode == "local": # Standard config, to file run_config = air.RunConfig( @@ -282,6 +303,7 @@ def invoke_tuning_run( checkpoint_at_end=False, # Disable final checkpoint ), stop=stoppers, + progress_reporter=progress_reporter, ) elif args.run_mode == "remote": # MLFlow, to MLFlow server @@ -298,6 +320,7 @@ def invoke_tuning_run( callbacks=[ProcessCleanupCallback(), mlflow_callback], checkpoint_config=ray.train.CheckpointConfig(checkpoint_frequency=0, checkpoint_at_end=False), stop=stoppers, + progress_reporter=progress_reporter, ) else: raise ValueError("Unrecognized run mode.") @@ -335,13 +358,13 @@ def __init__(self, cfg: dict): """ Runner args include command line arguments passed to the task. For example: - cfg["runner_args"]["headless_singleton"] = "--headless" - cfg["runner_args"]["enable_cameras_singleton"] = "--enable_cameras" + cfg["runner_args"]["video_singleton"] = "--video" """ assert "runner_args" in cfg, "No runner arguments specified." + cfg["runner_args"].setdefault("--rl_library", "rsl_rl") """ Task is the desired task to train on. For example: - cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-RGB-TheiaTiny-v0"]) + cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-Camera"]) """ assert "--task" in cfg["runner_args"], "No task specified." """ @@ -373,10 +396,7 @@ def __init__(self, cfg: dict): "--run_mode", choices=["local", "remote"], default="remote", - help=( - "Set to local to use ./isaaclab.sh -p python, set to " - "remote to use /workspace/isaaclab/isaaclab.sh -p python" - ), + help=("Set to local to use uv run python, set to remote to use /workspace/isaaclab/isaaclab.sh -p python"), ) parser.add_argument( "--workflow", @@ -435,6 +455,16 @@ def __init__(self, cfg: dict): default=MAX_LOG_EXTRACTION_ERRORS, help="Max number number of LogExtractionError failures before we abort the whole tuning run.", ) + parser.add_argument( + "--progress_reporter", + type=str, + default=None, + help=( + "Optional: name of a custom reporter class defined in the cfg_file. " + "Must subclass ray.tune.ProgressReporter " + "(e.g., CustomCartpoleProgressReporter)." + ), + ) parser.add_argument( "--stopper", type=str, @@ -508,7 +538,16 @@ def __init__(self, cfg: dict): else: raise TypeError(f"[ERROR]: Unsupported stop criteria type: {type(stopper)}") print(f"[INFO]: Loaded custom stop criteria from '{args.stopper}'") - invoke_tuning_run(cfg, args, stopper=stopper) + # Load optional progress reporter config + progress_reporter = None + if args.progress_reporter and hasattr(module, args.progress_reporter): + progress_reporter = getattr(module, args.progress_reporter) + if isinstance(progress_reporter, type) and issubclass(progress_reporter, tune.ProgressReporter): + progress_reporter = progress_reporter() + else: + raise TypeError(f"[ERROR]: {args.progress_reporter} is not a valid ProgressReporter.") + print(f"[INFO]: Loaded custom progress reporter from '{args.progress_reporter}'") + invoke_tuning_run(cfg, args, progress_reporter=progress_reporter, stopper=stopper) else: raise AttributeError(f"[ERROR]:Class '{class_name}' not found in {file_path}") diff --git a/scripts/reinforcement_learning/ray/util.py b/scripts/reinforcement_learning/ray/util.py index c6eb79e0..c14b1f0b 100644 --- a/scripts/reinforcement_learning/ray/util.py +++ b/scripts/reinforcement_learning/ray/util.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import argparse @@ -60,7 +60,7 @@ def get_latest_scalars(path: str) -> dict: def get_invocation_command_from_cfg( cfg: dict, python_cmd: str = "/workspace/isaaclab/isaaclab.sh -p", - workflow: str = "scripts/reinforcement_learning/rl_games/train.py", + workflow: str = "scripts/reinforcement_learning/train.py", ) -> str: """Generate command with proper Hydra arguments""" runner_args = [] diff --git a/scripts/reinforcement_learning/ray/wrap_resources.py b/scripts/reinforcement_learning/ray/wrap_resources.py index 127b1764..7043b23e 100644 --- a/scripts/reinforcement_learning/ray/wrap_resources.py +++ b/scripts/reinforcement_learning/ray/wrap_resources.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -40,22 +40,22 @@ .. code-block:: bash # **Ensure that sub-jobs are separated by the ``+`` delimiter.** # Generic Templates----------------------------------- - ./isaaclab.sh -p scripts/reinforcement_learning/ray/wrap_resources.py -h + uv run python scripts/reinforcement_learning/ray/wrap_resources.py -h # No resource isolation; no parallelization: - ./isaaclab.sh -p scripts/reinforcement_learning/ray/wrap_resources.py + uv run python scripts/reinforcement_learning/ray/wrap_resources.py --sub_jobs ++ # Automatic Resource Isolation; Example A: needed for parallelization - ./isaaclab.sh -p scripts/reinforcement_learning/ray/wrap_resources.py \ + uv run python scripts/reinforcement_learning/ray/wrap_resources.py \ --num_workers \ --sub_jobs + # Manual Resource Isolation; Example B: needed for parallelization - ./isaaclab.sh -p scripts/reinforcement_learning/ray/wrap_resources.py --num_cpu_per_worker \ + uv run python scripts/reinforcement_learning/ray/wrap_resources.py --num_cpu_per_worker \ --gpu_per_worker --ram_gb_per_worker --sub_jobs + # Manual Resource Isolation; Example C: Needed for parallelization, for heterogeneous workloads - ./isaaclab.sh -p scripts/reinforcement_learning/ray/wrap_resources.py --num_cpu_per_worker \ + uv run python scripts/reinforcement_learning/ray/wrap_resources.py --num_cpu_per_worker \ --gpu_per_worker --ram_gb_per_worker --sub_jobs + # to see all arguments - ./isaaclab.sh -p scripts/reinforcement_learning/ray/wrap_resources.py -h + uv run python scripts/reinforcement_learning/ray/wrap_resources.py -h """ import argparse diff --git a/scripts/reinforcement_learning/rsl_rl/cli_args.py b/scripts/reinforcement_learning/rsl_rl/cli_args.py index 215003b5..c09fd85e 100644 --- a/scripts/reinforcement_learning/rsl_rl/cli_args.py +++ b/scripts/reinforcement_learning/rsl_rl/cli_args.py @@ -29,7 +29,7 @@ def add_rsl_rl_args(parser: argparse.ArgumentParser): ) arg_group.add_argument("--run_name", type=str, default=None, help="Run name suffix to the log directory.") # -- load arguments - arg_group.add_argument("--resume", action="store_true", default=False, help="Whether to resume from a checkpoint.") + arg_group.add_argument("--resume", action="store_true", default=None, help="Whether to resume from a checkpoint.") arg_group.add_argument("--load_run", type=str, default=None, help="Name of the run folder to resume from.") arg_group.add_argument("--checkpoint", type=str, default=None, help="Checkpoint file to resume from.") # -- logger arguments @@ -81,6 +81,10 @@ def update_rsl_rl_cfg(agent_cfg: RslRlBaseRunnerCfg, args_cli: argparse.Namespac agent_cfg.load_run = args_cli.load_run if args_cli.checkpoint is not None: agent_cfg.load_checkpoint = args_cli.checkpoint + if args_cli.experiment_name is not None: + agent_cfg.experiment_name = args_cli.experiment_name + if getattr(args_cli, "device", None) is not None: + agent_cfg.device = args_cli.device if args_cli.run_name is not None: agent_cfg.run_name = args_cli.run_name if args_cli.logger is not None: diff --git a/scripts/reinforcement_learning/rsl_rl/play.py b/scripts/reinforcement_learning/rsl_rl/play.py index 611e5047..f0c0f102 100644 --- a/scripts/reinforcement_learning/rsl_rl/play.py +++ b/scripts/reinforcement_learning/rsl_rl/play.py @@ -71,8 +71,9 @@ from isaaclab.utils.dict import print_dict from isaaclab_rl.rsl_rl import RslRlBaseRunnerCfg, RslRlVecEnvWrapper + +from uwlab_rl.rsl_rl.exporter import export_policy_as_jit from isaaclab_rl.utils.pretrained_checkpoint import get_published_pretrained_checkpoint -from uwlab_rl.rsl_rl.exporter import export_policy_as_jit, export_policy_as_onnx import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 @@ -155,27 +156,11 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # obtain the trained policy for inference policy = runner.get_inference_policy(device=env.unwrapped.device) - # extract the neural network module - # we do this in a try-except to maintain backwards compatibility. - try: - # version 2.3 onwards - policy_nn = runner.alg.policy - except AttributeError: - # version 2.2 and below - policy_nn = runner.alg.actor_critic - - # extract the normalizer - if hasattr(policy_nn, "actor_obs_normalizer"): - normalizer = policy_nn.actor_obs_normalizer - elif hasattr(policy_nn, "student_obs_normalizer"): - normalizer = policy_nn.student_obs_normalizer - else: - normalizer = None - - # export policy to onnx/jit + # export the trained policy to JIT and ONNX formats. The JIT export carries the output + # distribution (``compute_distribution``), which demo collection samples from. export_model_dir = os.path.join(os.path.dirname(resume_path), "exported") - export_policy_as_jit(policy_nn, normalizer=normalizer, path=export_model_dir, filename="policy.pt") - export_policy_as_onnx(policy_nn, normalizer=normalizer, path=export_model_dir, filename="policy.onnx") + export_policy_as_jit(getattr(runner.alg, "_raw_actor", runner.alg.actor), path=export_model_dir, filename="policy.pt") + runner.export_policy_to_onnx(path=export_model_dir, filename="policy.onnx") dt = env.unwrapped.step_dt @@ -192,7 +177,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # env stepping obs, _, dones, _ = env.step(actions) # reset recurrent states for episodes that have terminated - policy_nn.reset(dones) + policy.reset(dones) if args_cli.video: timestep += 1 # Exit the play loop after recording one video diff --git a/scripts/reinforcement_learning/rsl_rl/train.py b/scripts/reinforcement_learning/rsl_rl/train.py index bee6d5dc..36bd4703 100644 --- a/scripts/reinforcement_learning/rsl_rl/train.py +++ b/scripts/reinforcement_learning/rsl_rl/train.py @@ -58,18 +58,14 @@ """Check for minimum supported RSL-RL version.""" import importlib.metadata as metadata -import platform from packaging import version # check minimum supported rsl-rl version -RSL_RL_VERSION = "3.0.1" +RSL_RL_VERSION = "5.4.1" installed_version = metadata.version("rsl-rl-lib") if version.parse(installed_version) < version.parse(RSL_RL_VERSION): - if platform.system() == "Windows": - cmd = [r".\isaaclab.bat", "-p", "-m", "pip", "install", f"rsl-rl-lib=={RSL_RL_VERSION}"] - else: - cmd = ["./isaaclab.sh", "-p", "-m", "pip", "install", f"rsl-rl-lib=={RSL_RL_VERSION}"] + cmd = [sys.executable, "-m", "pip", "install", "-e", "source/uwlab_rl[rsl-rl]"] print( f"Please install the correct version of RSL-RL.\nExisting version is: '{installed_version}'" f" and required version is: '{RSL_RL_VERSION}'.\nTo install the correct version, run:" diff --git a/scripts/reinforcement_learning/train.py b/scripts/reinforcement_learning/train.py new file mode 100644 index 00000000..8e1d530c --- /dev/null +++ b/scripts/reinforcement_learning/train.py @@ -0,0 +1,30 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Unified training executable for Isaac Lab reinforcement learning workflows.""" + +# Warp captures ``enable_backward`` when a module is created, which happens at import +# time, so it has to be set before importing anything that defines Warp kernels. +# Isaac Lab does not use Warp autodiff; skipping adjoint codegen roughly halves the +# time spent building kernels on a cold kernel cache. +import warp as wp + +wp.config.enable_backward = False + +from isaaclab_rl.entrypoints import run_train_cli # noqa: E402 + + +def main(argv: list[str] | None = None) -> int: + """Run the selected reinforcement learning training library.""" + return run_train_cli(argv) + + +if __name__ == "__main__": + # ``record`` writes the failing rank's traceback to the error file that torchrun reports as the + # root cause. Without it, a crash on a non-zero rank surfaces only as an exit code, which is + # invisible when console output is filtered to rank 0. Outside torchrun it is a no-op. + from torch.distributed.elastic.multiprocessing.errors import record + + raise SystemExit(record(main)()) diff --git a/scripts/reinforcement_learning/train_multigpu.py b/scripts/reinforcement_learning/train_multigpu.py new file mode 100644 index 00000000..cb094246 --- /dev/null +++ b/scripts/reinforcement_learning/train_multigpu.py @@ -0,0 +1,17 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Multi-GPU training executable for Isaac Lab reinforcement learning workflows.""" + +from isaaclab_rl.entrypoints import run_train_multigpu_cli + + +def main(argv: list[str] | None = None) -> int: + """Launch multi-GPU training with the selected distributed launcher.""" + return run_train_multigpu_cli(argv) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/tools/blender_obj.py b/scripts/tools/blender_obj.py index 16b55133..c03a525f 100644 --- a/scripts/tools/blender_obj.py +++ b/scripts/tools/blender_obj.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -17,10 +17,11 @@ The script was tested on Blender 3.2 on Ubuntu 20.04LTS. """ -import bpy import os import sys +import bpy + def parse_cli_args(): """Parse the input command line arguments.""" diff --git a/scripts/tools/check_instanceable.py b/scripts/tools/check_instanceable.py index f0da0e26..8cd5cce3 100644 --- a/scripts/tools/check_instanceable.py +++ b/scripts/tools/check_instanceable.py @@ -1,19 +1,21 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """ This script uses the cloner API to check if asset has been instanced properly. +An asset path may be a local file or a Nucleus/HTTPS URL; remote assets are downloaded before use. + Usage with different inputs (replace `` and `` with the path to the original asset and the instanced asset respectively): ```bash -./isaaclab.sh -p source/tools/check_instanceable.py -n 4096 --headless --physics -./isaaclab.sh -p source/tools/check_instanceable.py -n 4096 --headless --physics -./isaaclab.sh -p source/tools/check_instanceable.py -n 4096 --headless -./isaaclab.sh -p source/tools/check_instanceable.py -n 4096 --headless +uv run python scripts/tools/check_instanceable.py -n 4096 --physics +uv run python scripts/tools/check_instanceable.py -n 4096 --physics +uv run python scripts/tools/check_instanceable.py -n 4096 +uv run python scripts/tools/check_instanceable.py -n 4096 ``` Output from the above commands: @@ -42,7 +44,6 @@ import argparse import contextlib -import os from isaaclab.app import AppLauncher @@ -64,14 +65,16 @@ """Rest everything follows.""" -import isaacsim.core.utils.prims as prim_utils -from isaacsim.core.api.simulation_context import SimulationContext +from isaaclab.sim.utils import enable_extension + +enable_extension("isaacsim.core.cloner") + from isaacsim.core.cloner import GridCloner -from isaacsim.core.utils.carb import set_carb_setting -from isaacsim.core.utils.stage import get_current_stage +import isaaclab.sim as sim_utils +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.utils import Timer -from isaaclab.utils.assets import check_file_path +from isaaclab.utils.assets import check_file_path, retrieve_file_path def main(): @@ -80,33 +83,27 @@ def main(): if not check_file_path(args_cli.input): raise ValueError(f"Invalid file path: {args_cli.input}") # Load kit helper - sim = SimulationContext( - stage_units_in_meters=1.0, physics_dt=0.01, rendering_dt=0.01, backend="torch", device="cuda:0" - ) + sim = SimulationContext(SimulationCfg(dt=0.01)) # get stage handle - stage = get_current_stage() - - # enable fabric which avoids passing data over to USD structure - # this speeds up the read-write operation of GPU buffers - if sim.get_physics_context().use_gpu_pipeline: - sim.get_physics_context().enable_fabric(True) - # increase GPU buffer dimensions - sim.get_physics_context().set_gpu_found_lost_aggregate_pairs_capacity(2**25) - sim.get_physics_context().set_gpu_total_aggregate_pairs_capacity(2**21) + stage = sim_utils.get_current_stage() + + # Fabric and PhysX GPU buffers are configured through SimulationCfg/PhysxCfg defaults. # enable hydra scene-graph instancing # this is needed to visualize the scene when fabric is enabled - set_carb_setting(sim._settings, "/persistent/omnihydra/useSceneGraphInstancing", True) + sim.set_setting("/persistent/omnihydra/useSceneGraphInstancing", True) # Create interface to clone the scene cloner = GridCloner(spacing=args_cli.spacing, stage=stage) cloner.define_base_env("/World/envs") - prim_utils.define_prim("/World/envs/env_0") + stage.DefinePrim("/World/envs/env_0", "Xform") # Spawn things into stage - prim_utils.create_prim("/World/Light", "DistantLight") + sim_utils.create_prim("/World/Light", "DistantLight") - # Everything under the namespace "/World/envs/env_0" will be cloned - prim_utils.create_prim("/World/envs/env_0/Asset", "Xform", usd_path=os.path.abspath(args_cli.input)) + # Everything under the namespace "/World/envs/env_0" will be cloned. + # Resolve through retrieve_file_path so Nucleus/HTTPS inputs are downloaded first; applying + # os.path.abspath() to a URL would prepend the working directory and corrupt it. + sim_utils.create_prim("/World/envs/env_0/Asset", "Xform", usd_path=retrieve_file_path(args_cli.input)) # Clone the scene num_clones = args_cli.num_clones diff --git a/scripts/tools/convert_instanceable.py b/scripts/tools/convert_instanceable.py index 73c2dd6c..7713bdc7 100644 --- a/scripts/tools/convert_instanceable.py +++ b/scripts/tools/convert_instanceable.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -89,7 +89,6 @@ def main(): - # Define conversion time given conversion_type = args_cli.conversion_type.lower() # Warning if conversion type input is not valid diff --git a/scripts/tools/convert_mesh.py b/scripts/tools/convert_mesh.py index 7139c62e..0493bc3f 100644 --- a/scripts/tools/convert_mesh.py +++ b/scripts/tools/convert_mesh.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -31,7 +31,8 @@ -h, --help Show this help message and exit --make-instanceable, Make the asset instanceable for efficient cloning. (default: False) --collision-approximation The method used for approximating collision mesh. Defaults to convexDecomposition. - Set to \"none\" to not add a collision mesh to the converted mesh. (default: convexDecomposition) + Set to \"none\" to not add a collision mesh to the converted mesh. + (default: convexDecomposition) --mass The mass (in kg) to assign to the converted asset. (default: None) """ @@ -89,13 +90,9 @@ """Rest everything follows.""" -import contextlib import os -import carb -import isaacsim.core.utils.stage as stage_utils -import omni.kit.app - +import isaaclab.sim as sim_utils from isaaclab.sim.converters import MeshConverter, MeshConverterCfg from isaaclab.sim.schemas import schemas_cfg from isaaclab.utils.assets import check_file_path @@ -176,25 +173,9 @@ def main(): print("-" * 80) print("-" * 80) - # Determine if there is a GUI to update: - # acquire settings interface - carb_settings_iface = carb.settings.get_settings() - # read flag for whether a local GUI is enabled - local_gui = carb_settings_iface.get("/app/window/enabled") - # read flag for whether livestreaming GUI is enabled - livestream_gui = carb_settings_iface.get("/app/livestream/enabled") - - # Simulate scene (if not headless) - if local_gui or livestream_gui: - # Open the stage with USD - stage_utils.open_stage(mesh_converter.usd_path) - # Reinitialize the simulation - app = omni.kit.app.get_app_interface() - # Run simulation - with contextlib.suppress(KeyboardInterrupt): - while app.is_running(): - # perform step - app.update() + # Show the converted asset if the launch resolved to a window or livestream + if AppLauncher.has_gui(): + sim_utils.show_stage_in_viewport(mesh_converter.usd_path) if __name__ == "__main__": diff --git a/scripts/tools/convert_mjcf.py b/scripts/tools/convert_mjcf.py index fa05a42b..e72c0f60 100644 --- a/scripts/tools/convert_mjcf.py +++ b/scripts/tools/convert_mjcf.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,66 +9,139 @@ MuJoCo XML Format (MJCF) is an XML file format used in MuJoCo to describe all elements of a robot. For more information, see: http://www.mujoco.org/book/XMLreference.html -This script uses the MJCF importer extension from Isaac Sim (``isaacsim.asset.importer.mjcf``) to convert -a MJCF asset into USD format. It is designed as a convenience script for command-line use. For more information -on the MJCF importer, see the documentation for the extension: +This script uses the MJCF importer API (``isaacsim.asset.importer.mjcf``) from Isaac Sim or its standalone +wheel to convert a MJCF asset into USD format. It is designed as a convenience script for command-line use. +For more information on the MJCF importer, see the documentation for the extension: https://docs.isaacsim.omniverse.nvidia.com/latest/robot_setup/ext_isaacsim_asset_importer_mjcf.html +The requested output file is a USD entry layer. Keep the importer-generated asset +directory beside it so its relative references remain available. + positional arguments: - input The path to the input URDF file. + input The path to the input MJCF file. output The path to store the USD file. optional arguments: -h, --help Show this help message and exit - --fix-base Fix the base to where it is imported. (default: False) - --import-sites Import sites by parse tag. (default: True) - --make-instanceable Make the asset instanceable for efficient cloning. (default: False) + --merge_mesh Merge meshes where possible to optimize the model. (default: False) + --collision_from_visuals Generate collision geometry from visual geometries. (default: False) + --collision_type Type of collision geometry to use. (default: "Convex Hull") + --self_collision Activate self-collisions between links. (default: False) + --import_physics_scene Import the physics scene from the MJCF file. (default: False) + +The standard launcher arguments are also accepted. In particular, ``--viz`` previews the converted +asset: ``--viz kit`` opens it in the Isaac Sim viewport, while ``--viz newton`` (or ``rerun`` / +``viser``) opens it kitlessly. Run with ``--help`` for the full list. """ -"""Launch Isaac Sim Simulator first.""" +"""Parse CLI first so we can decide whether to launch Isaac Sim Kit.""" import argparse -from isaaclab.app import AppLauncher +from isaaclab.app import AppLauncher, add_launcher_args, launch_simulation +from isaaclab.utils.version import standalone_importers_available -# add argparse arguments parser = argparse.ArgumentParser(description="Utility to convert a MJCF into USD format.") parser.add_argument("input", type=str, help="The path to the input MJCF file.") parser.add_argument("output", type=str, help="The path to store the USD file.") -parser.add_argument("--fix-base", action="store_true", default=False, help="Fix the base to where it is imported.") parser.add_argument( - "--import-sites", action="store_true", default=False, help="Import sites by parsing the tag." + "--merge_mesh", + "--merge-mesh", + action="store_true", + default=False, + help="Merge meshes where possible to optimize the model.", ) parser.add_argument( - "--make-instanceable", + "--collision_from_visuals", + "--collision-from-visuals", action="store_true", default=False, - help="Make the asset instanceable for efficient cloning.", + help="Generate collision geometry from visual geometries.", ) - -# append AppLauncher cli args -AppLauncher.add_app_launcher_args(parser) -# parse the arguments +parser.add_argument( + "--collision_type", + "--collision-type", + type=str, + default="Convex Hull", + choices=["Convex Hull", "Convex Decomposition", "Bounding Sphere", "Bounding Cube"], + help='Type of collision geometry to use. Defaults to "Convex Hull".', +) +parser.add_argument( + "--self_collision", + "--self-collision", + action="store_true", + default=False, + help="Activate self-collisions between links of the articulation.", +) +parser.add_argument( + "--import_physics_scene", + "--import-physics-scene", + action="store_true", + default=False, + help="Import the physics scene (worldbody, defaults) from the MJCF file.", +) +add_launcher_args(parser) args_cli = parser.parse_args() -# launch omniverse app -app_launcher = AppLauncher(args_cli) -simulation_app = app_launcher.app +# Prefer kit-less: it skips Kit startup and the kitless visualizers can host the preview. +args_cli.require_kit = not standalone_importers_available() -"""Rest everything follows.""" +# ``launch_simulation`` receives a bare ``PhysicsCfg()`` placeholder, so name the backend the +# runtime provides; without it the preview builds a simulation with no physics manager. +args_cli.physics = "isaacsim_physx" if args_cli.require_kit else "newton_mjwarp" -import contextlib -import os - -import carb -import isaacsim.core.utils.stage as stage_utils -import omni.kit.app +# Report the missing importer before converting anything. Without this the launcher reports only +# that Isaac Sim is absent, which does not mention the wheel that would make this run kitlessly. +if args_cli.require_kit and not AppLauncher.is_available(): + raise ImportError( + "MJCF conversion requires either the full Isaac Sim runtime or the standalone" + " 'isaacsim-asset-isolated' importer wheel, but neither is installed." + ) -from isaaclab.sim.converters import MjcfConverter, MjcfConverterCfg -from isaaclab.utils.assets import check_file_path -from isaaclab.utils.dict import print_dict +import os # noqa: E402 + +import isaaclab.sim as sim_utils # noqa: E402 +from isaaclab.physics import PhysicsCfg # noqa: E402 +from isaaclab.sim.converters import MjcfConverter, MjcfConverterCfg # noqa: E402 +from isaaclab.utils.assets import check_file_path # noqa: E402 +from isaaclab.utils.dict import print_dict # noqa: E402 +from usd_output import write_usd_entry_layer + + +def preview(usd_path: str, physics_cfg: PhysicsCfg) -> None: + """Open the converted asset in the visualizer selected on the command line. + + Args: + usd_path: Path of the generated USD file to display. + physics_cfg: Physics config resolved by :func:`~isaaclab.app.launch_simulation`. + """ + visualizers = args_cli.visualizer or [] + if not visualizers: + return + + if "kit" in visualizers: + # a Kit app that resolved without a GUI has no viewport to display the asset in + if AppLauncher.has_gui(): + sim_utils.show_stage_in_viewport(usd_path) + return + + # Kitless preview: the physics backend ingests the USD stage and every visualizer renders the + # shared scene data, so no backend-specific code is needed here. Physics is not stepped -- the + # asset is shown in its imported pose until the visualizer window is closed. + sim = sim_utils.SimulationContext(sim_utils.SimulationCfg(device=args_cli.device, physics=physics_cfg)) + light_cfg = sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + light_cfg.func("/World/Light", light_cfg) + asset_cfg = sim_utils.UsdFileCfg(usd_path=usd_path) + asset_cfg.func("/World/ConvertedAsset", asset_cfg) + sim.reset() + + # Checked per visualizer rather than through ``SimulationContext.is_headless_or_exist_active_visualizer``: + # that predicate also reports True for an empty visualizer list (headless stepping), and ``render`` + # drops visualizers once they close, so the preview would never exit. + while any(viz.is_running() and not viz.is_closed for viz in sim.visualizers): + sim.render() def main(): @@ -87,11 +160,12 @@ def main(): mjcf_converter_cfg = MjcfConverterCfg( asset_path=mjcf_path, usd_dir=os.path.dirname(dest_path), - usd_file_name=os.path.basename(dest_path), - fix_base=args_cli.fix_base, - import_sites=args_cli.import_sites, force_usd_conversion=True, - make_instanceable=args_cli.make_instanceable, + merge_mesh=args_cli.merge_mesh, + collision_from_visuals=args_cli.collision_from_visuals, + collision_type=args_cli.collision_type, + self_collision=args_cli.self_collision, + import_physics_scene=args_cli.import_physics_scene, ) # Print info @@ -103,37 +177,18 @@ def main(): print("-" * 80) print("-" * 80) - # Create mjcf converter and import the file - mjcf_converter = MjcfConverter(mjcf_converter_cfg) - # print output - print("MJCF importer output:") - print(f"Generated USD file: {mjcf_converter.usd_path}") - print("-" * 80) - print("-" * 80) + with launch_simulation(cfg=PhysicsCfg(), launcher_args=args_cli) as physics_cfg: + # Create mjcf converter and import the file + mjcf_converter = MjcfConverter(mjcf_converter_cfg) + output_path = write_usd_entry_layer(mjcf_converter.usd_path, dest_path) + # print output + print("MJCF importer output:") + print(f"Generated USD file: {output_path}") + print("-" * 80) + print("-" * 80) - # Determine if there is a GUI to update: - # acquire settings interface - carb_settings_iface = carb.settings.get_settings() - # read flag for whether a local GUI is enabled - local_gui = carb_settings_iface.get("/app/window/enabled") - # read flag for whether livestreaming GUI is enabled - livestream_gui = carb_settings_iface.get("/app/livestream/enabled") - - # Simulate scene (if not headless) - if local_gui or livestream_gui: - # Open the stage with USD - stage_utils.open_stage(mjcf_converter.usd_path) - # Reinitialize the simulation - app = omni.kit.app.get_app_interface() - # Run simulation - with contextlib.suppress(KeyboardInterrupt): - while app.is_running(): - # perform step - app.update() + preview(output_path, physics_cfg) if __name__ == "__main__": - # run the main function main() - # close sim app - simulation_app.close() diff --git a/scripts/tools/convert_urdf.py b/scripts/tools/convert_urdf.py index aaa5ddb1..69e91e13 100644 --- a/scripts/tools/convert_urdf.py +++ b/scripts/tools/convert_urdf.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,84 +9,135 @@ Unified Robot Description Format (URDF) is an XML file format used in ROS to describe all elements of a robot. For more information, see: http://wiki.ros.org/urdf -This script uses the URDF importer extension from Isaac Sim (``isaacsim.asset.importer.urdf``) to convert a -URDF asset into USD format. It is designed as a convenience script for command-line use. For more -information on the URDF importer, see the documentation for the extension: +This script uses the URDF importer API (``isaacsim.asset.importer.urdf``) from Isaac Sim or its standalone +wheel to convert a URDF asset into USD format. It is designed as a convenience script for command-line use. +For more information on the URDF importer, see the documentation for the extension: https://docs.isaacsim.omniverse.nvidia.com/latest/robot_setup/ext_isaacsim_asset_importer_urdf.html +The requested output file is a USD entry layer. Keep the importer-generated asset +directory beside it so its relative references remain available. + positional arguments: input The path to the input URDF file. output The path to store the USD file. optional arguments: -h, --help Show this help message and exit - --merge-joints Consolidate links that are connected by fixed joints. (default: False) - --fix-base Fix the base to where it is imported. (default: False) - --joint-stiffness The stiffness of the joint drive. (default: 100.0) - --joint-damping The damping of the joint drive. (default: 1.0) - --joint-target-type The type of control to use for the joint drive. (default: "position") + --merge_joints Consolidate links that are connected by fixed joints. (default: False) + --fix_base Fix the base to where it is imported. (default: False) + --joint_stiffness The stiffness of the joint drive. (default: 100.0) + --joint_damping The damping of the joint drive. (default: 1.0) + --joint_target_type The type of control to use for the joint drive. (default: "position") + +The standard launcher arguments are also accepted. In particular, ``--viz`` previews the converted +asset: ``--viz kit`` opens it in the Isaac Sim viewport, while ``--viz newton`` (or ``rerun`` / +``viser``) opens it kitlessly. Run with ``--help`` for the full list. """ -"""Launch Isaac Sim Simulator first.""" +"""Parse CLI first so we can decide whether to launch Isaac Sim Kit.""" import argparse -from isaaclab.app import AppLauncher +from isaaclab.app import AppLauncher, add_launcher_args, launch_simulation +from isaaclab.utils.version import standalone_importers_available -# add argparse arguments parser = argparse.ArgumentParser(description="Utility to convert a URDF into USD format.") parser.add_argument("input", type=str, help="The path to the input URDF file.") parser.add_argument("output", type=str, help="The path to store the USD file.") parser.add_argument( + "--merge_joints", "--merge-joints", action="store_true", default=False, help="Consolidate links that are connected by fixed joints.", ) -parser.add_argument("--fix-base", action="store_true", default=False, help="Fix the base to where it is imported.") parser.add_argument( + "--fix_base", "--fix-base", action="store_true", default=False, help="Fix the base to where it is imported." +) +parser.add_argument( + "--joint_stiffness", "--joint-stiffness", type=float, default=100.0, help="The stiffness of the joint drive.", ) parser.add_argument( + "--joint_damping", "--joint-damping", type=float, default=1.0, help="The damping of the joint drive.", ) parser.add_argument( + "--joint_target_type", "--joint-target-type", type=str, default="position", choices=["position", "velocity", "none"], help="The type of control to use for the joint drive.", ) - -# append AppLauncher cli args -AppLauncher.add_app_launcher_args(parser) -# parse the arguments +add_launcher_args(parser) args_cli = parser.parse_args() -# launch omniverse app -app_launcher = AppLauncher(args_cli) -simulation_app = app_launcher.app +# Prefer kit-less: it skips Kit startup and the kitless visualizers can host the preview. +args_cli.require_kit = not standalone_importers_available() -"""Rest everything follows.""" +# ``launch_simulation`` receives a bare ``PhysicsCfg()`` placeholder, so name the backend the +# runtime provides; without it the preview builds a simulation with no physics manager. +args_cli.physics = "isaacsim_physx" if args_cli.require_kit else "newton_mjwarp" -import contextlib -import os - -import carb -import isaacsim.core.utils.stage as stage_utils -import omni.kit.app +# Report the missing importer before converting anything. Without this the launcher reports only +# that Isaac Sim is absent, which does not mention the wheel that would make this run kitlessly. +if args_cli.require_kit and not AppLauncher.is_available(): + raise ImportError( + "URDF conversion requires either the full Isaac Sim runtime or the standalone" + " 'isaacsim-asset-isolated' importer wheel, but neither is installed." + ) -from isaaclab.sim.converters import UrdfConverter, UrdfConverterCfg -from isaaclab.utils.assets import check_file_path -from isaaclab.utils.dict import print_dict +import os # noqa: E402 + +import isaaclab.sim as sim_utils # noqa: E402 +from isaaclab.physics import PhysicsCfg # noqa: E402 +from isaaclab.sim.converters import UrdfConverter, UrdfConverterCfg # noqa: E402 +from isaaclab.utils.assets import check_file_path # noqa: E402 +from isaaclab.utils.dict import print_dict # noqa: E402 +from usd_output import write_usd_entry_layer + + +def preview(usd_path: str, physics_cfg: PhysicsCfg) -> None: + """Open the converted asset in the visualizer selected on the command line. + + Args: + usd_path: Path of the generated USD file to display. + physics_cfg: Physics config resolved by :func:`~isaaclab.app.launch_simulation`. + """ + visualizers = args_cli.visualizer or [] + if not visualizers: + return + + if "kit" in visualizers: + # a Kit app that resolved without a GUI has no viewport to display the asset in + if AppLauncher.has_gui(): + sim_utils.show_stage_in_viewport(usd_path) + return + + # Kitless preview: the physics backend ingests the USD stage and every visualizer renders the + # shared scene data, so no backend-specific code is needed here. Physics is not stepped -- the + # asset is shown in its imported pose until the visualizer window is closed. + sim = sim_utils.SimulationContext(sim_utils.SimulationCfg(device=args_cli.device, physics=physics_cfg)) + light_cfg = sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + light_cfg.func("/World/Light", light_cfg) + asset_cfg = sim_utils.UsdFileCfg(usd_path=usd_path) + asset_cfg.func("/World/ConvertedAsset", asset_cfg) + sim.reset() + + # Checked per visualizer rather than through ``SimulationContext.is_headless_or_exist_active_visualizer``: + # that predicate also reports True for an empty visualizer list (headless stepping), and ``render`` + # drops visualizers once they close, so the preview would never exit. + while any(viz.is_running() and not viz.is_closed for viz in sim.visualizers): + sim.render() def main(): @@ -102,10 +153,11 @@ def main(): dest_path = os.path.abspath(dest_path) # Create Urdf converter config + # The importer owns its structured asset filenames and relative references. + # An entry layer below preserves the requested output filename. urdf_converter_cfg = UrdfConverterCfg( asset_path=urdf_path, usd_dir=os.path.dirname(dest_path), - usd_file_name=os.path.basename(dest_path), fix_base=args_cli.fix_base, merge_fixed_joints=args_cli.merge_joints, force_usd_conversion=True, @@ -127,37 +179,18 @@ def main(): print("-" * 80) print("-" * 80) - # Create Urdf converter and import the file - urdf_converter = UrdfConverter(urdf_converter_cfg) - # print output - print("URDF importer output:") - print(f"Generated USD file: {urdf_converter.usd_path}") - print("-" * 80) - print("-" * 80) + with launch_simulation(cfg=PhysicsCfg(), launcher_args=args_cli) as physics_cfg: + # Create Urdf converter and import the file + urdf_converter = UrdfConverter(urdf_converter_cfg) + output_path = write_usd_entry_layer(urdf_converter.usd_path, dest_path) + # print output + print("URDF importer output:") + print(f"Generated USD file: {output_path}") + print("-" * 80) + print("-" * 80) - # Determine if there is a GUI to update: - # acquire settings interface - carb_settings_iface = carb.settings.get_settings() - # read flag for whether a local GUI is enabled - local_gui = carb_settings_iface.get("/app/window/enabled") - # read flag for whether livestreaming GUI is enabled - livestream_gui = carb_settings_iface.get("/app/livestream/enabled") - - # Simulate scene (if not headless) - if local_gui or livestream_gui: - # Open the stage with USD - stage_utils.open_stage(urdf_converter.usd_path) - # Reinitialize the simulation - app = omni.kit.app.get_app_interface() - # Run simulation - with contextlib.suppress(KeyboardInterrupt): - while app.is_running(): - # perform step - app.update() + preview(output_path, physics_cfg) if __name__ == "__main__": - # run the main function main() - # close sim app - simulation_app.close() diff --git a/scripts/tools/cosmos/cosmos_prompt_gen.py b/scripts/tools/cosmos/cosmos_prompt_gen.py index b0f34513..32db884a 100644 --- a/scripts/tools/cosmos/cosmos_prompt_gen.py +++ b/scripts/tools/cosmos/cosmos_prompt_gen.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2024-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/scripts/tools/hdf5_to_mp4.py b/scripts/tools/hdf5_to_mp4.py index 98fc1a9b..a0f4f7ea 100644 --- a/scripts/tools/hdf5_to_mp4.py +++ b/scripts/tools/hdf5_to_mp4.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2024-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -15,21 +15,21 @@ --output_dir Directory to save the output MP4 files. optional arguments: - --input_keys List of input keys to process from the HDF5 file. (default: ["table_cam", "wrist_cam", "table_cam_segmentation", "table_cam_normals", "table_cam_shaded_segmentation"]) + --input_keys List of input keys to process from the HDF5 file. + (default: ["table_cam", "wrist_cam", "table_cam_segmentation", + "table_cam_normals", "table_cam_shaded_segmentation"]) --video_height Height of the output video in pixels. (default: 704) --video_width Width of the output video in pixels. (default: 1280) --framerate Frames per second for the output video. (default: 30) + --demo_id If provided, only export this specific demo_id. (default: None) """ -# Standard library imports import argparse -import h5py -import numpy as np - -# Third-party imports import os import cv2 +import h5py +import numpy as np # Constants DEFAULT_VIDEO_HEIGHT = 704 @@ -48,6 +48,18 @@ MAX_DEPTH = 1.5 +def _convert_frame_to_uint8(frame: np.ndarray) -> np.ndarray: + """Convert a video frame to the uint8 format expected by MP4 encoders.""" + if frame.dtype == np.uint8: + return frame + + if np.issubdtype(frame.dtype, np.floating): + frame = np.nan_to_num(frame, nan=0.0, posinf=1.0, neginf=0.0) + return np.clip(frame * 255.0, 0, 255).astype(np.uint8) + + return np.clip(frame, 0, 255).astype(np.uint8) + + def parse_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser(description="Convert HDF5 demonstration files to MP4 videos.") @@ -89,6 +101,12 @@ def parse_args(): default=DEFAULT_FRAMERATE, help="Frames per second for the output video.", ) + parser.add_argument( + "--demo_id", + type=int, + default=None, + help="If provided, only export this specific demo_id.", + ) args = parser.parse_args() @@ -141,15 +159,17 @@ def write_demo_to_mp4( # Process shaded segmentation frames elif "shaded_segmentation" in input_key: - seg = frame[..., :-1] + segmentation_frame = _convert_frame_to_uint8(frame) + seg = segmentation_frame[..., :-1] normals_key = input_key.replace("shaded_segmentation", "normals") normals = f[f"data/demo_{demo_id}/obs/{normals_key}"][ix] shade = 0.5 + (normals * LIGHT_SOURCE[None, None, :]).sum(axis=-1) * 0.5 - shaded_seg = (shade[..., None] * seg).astype(np.uint8) - frame = np.concatenate((shaded_seg, frame[..., -1:]), axis=-1) + shaded_seg = np.clip(shade[..., None] * seg, 0, 255).astype(np.uint8) + frame = np.concatenate((shaded_seg, segmentation_frame[..., -1:]), axis=-1) # Convert RGB to BGR if "depth" not in input_key: + frame = _convert_frame_to_uint8(frame) frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) else: frame = (frame[..., 0] - MIN_DEPTH) / (MAX_DEPTH - MIN_DEPTH) @@ -189,8 +209,14 @@ def main(): num_demos = get_num_demos(args.input_file) print(f"Found {num_demos} demonstrations in {args.input_file}") + if args.demo_id is not None: + demo_ids = [args.demo_id] + print(f"Exporting only demo_id {args.demo_id}") + else: + demo_ids = list(range(num_demos)) + # Convert each demonstration - for i in range(num_demos): + for i in demo_ids: frames_path = f"data/demo_{str(i)}/obs" for input_key in args.input_keys: write_demo_to_mp4( diff --git a/scripts/tools/merge_hdf5_datasets.py b/scripts/tools/merge_hdf5_datasets.py index 92329f77..a9fe1c63 100644 --- a/scripts/tools/merge_hdf5_datasets.py +++ b/scripts/tools/merge_hdf5_datasets.py @@ -1,12 +1,13 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import argparse -import h5py import os +import h5py + parser = argparse.ArgumentParser(description="Merge a set of HDF5 datasets.") parser.add_argument( "--input_files", @@ -30,7 +31,6 @@ def merge_datasets(): copy_attributes = True for filepath in args_cli.input_files: - with h5py.File(filepath, "r") as input: for episode, data in input["data"].items(): input.copy(f"data/{episode}", output, f"data/demo_{episode_idx}") diff --git a/scripts/tools/mp4_to_hdf5.py b/scripts/tools/mp4_to_hdf5.py index a4d8f43f..61f7b5b0 100644 --- a/scripts/tools/mp4_to_hdf5.py +++ b/scripts/tools/mp4_to_hdf5.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2024-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -17,16 +17,13 @@ --videos_dir Directory containing the visually augmented MP4 videos. """ -# Standard library imports import argparse import glob -import h5py -import numpy as np - -# Third-party imports import os import cv2 +import h5py +import numpy as np def parse_args(): diff --git a/scripts/tools/pretrained_checkpoint.py b/scripts/tools/pretrained_checkpoint.py deleted file mode 100644 index 96b8562d..00000000 --- a/scripts/tools/pretrained_checkpoint.py +++ /dev/null @@ -1,376 +0,0 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -""" -Script to manage pretrained checkpoints for our environments. -""" - -import argparse - -from isaaclab.app import AppLauncher - -# Initialize the parser -parser = argparse.ArgumentParser( - description=""" -Script used for the training and publishing of pre-trained checkpoints for Isaac Lab. - -Examples : - # Train an agent using the rl_games workflow on the Isaac-Cartpole-v0 environment. - pretrained_checkpoint.py --train rl_games:Isaac-Cartpole-v0 - # Train and publish the checkpoints for all workflows on only the direct Cartpole environments. - pretrained_checkpoint.py -tp "*:Isaac-Cartpole-*Direct-v0" \\ - --/persistent/isaaclab/asset_root/pretrained_checkpoints="/some/path" - # Review all repose cube jobs, excluding the Play tasks and skrl - pretrained_checkpoint.py -r "*:*Repose-Cube*" --exclude "*:*Play*" --exclude skrl:* - # Publish all results (that have been reviewed and approved). - pretrained_checkpoint.py --publish --all \\ - --/persistent/isaaclab/asset_root/pretrained_checkpoints="/some/path" -""", - formatter_class=argparse.RawTextHelpFormatter, -) - -# Add positional arguments that can accept zero or more values -parser.add_argument( - "jobs", - nargs="*", - help=""" -A job consists of a workflow and a task name separated by a colon (wildcards optional), for example : - rl_games:Isaac-Humanoid-*v0 - rsl_rl:Isaac-Ant-*-v0 - *:Isaac-Velocity-Flat-Spot-v0 -""", -) -parser.add_argument("-t", "--train", action="store_true", help="Train checkpoints for later publishing.") -parser.add_argument("-p", "--publish_checkpoint", action="store_true", help="Publish pre-trained checkpoints.") -parser.add_argument("-r", "--review", action="store_true", help="Review checkpoints.") -parser.add_argument("-l", "--list", action="store_true", help="List all available environments and workflows.") -parser.add_argument("-f", "--force", action="store_true", help="Force training when results already exist.") -parser.add_argument("-a", "--all", action="store_true", help="Run all valid workflow task pairs.") -parser.add_argument( - "-E", - "--exclude", - action="append", - type=str, - default=[], - help="Excludes jobs matching the argument, with wildcard support.", -) -parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") -parser.add_argument("--force_review", action="store_true", help="Forces review when one already exists.") -parser.add_argument("--force_publish", action="store_true", help="Publish checkpoints without review.") -parser.add_argument("--headless", action="store_true", help="Run training without the UI.") - -args, _ = parser.parse_known_args() - -# Need something to do -if len(args.jobs) == 0 and not args.all: - parser.error("Jobs must be provided, or --all.") - -# Must train, publish, review or list -if not (args.train or args.publish_checkpoint or args.review or args.list): - parser.error("A train, publish, review or list flag must be given.") - -# List excludes train and publish -if args.list and (args.train or args.publish_checkpoint): - parser.error("Can't train or publish when listing.") - -# launch omniverse app -app_launcher = AppLauncher(headless=True) -simulation_app = app_launcher.app - - -import csv - -# Now everything else -import fnmatch -import gymnasium as gym -import json -import numpy as np -import os -import subprocess -import sys - -import omni.client -from omni.client._omniclient import CopyBehavior - -from isaaclab.utils.pretrained_checkpoint import ( - WORKFLOW_EXPERIMENT_NAME_VARIABLE, - WORKFLOW_PLAYER, - WORKFLOW_TRAINER, - WORKFLOWS, - get_log_root_path, - get_pretrained_checkpoint_path, - get_pretrained_checkpoint_publish_path, - get_pretrained_checkpoint_review, - get_pretrained_checkpoint_review_path, - has_pretrained_checkpoint_job_finished, - has_pretrained_checkpoint_job_run, - has_pretrained_checkpoints_asset_root_dir, -) - -# Need somewhere to publish -if args.publish_checkpoint and not has_pretrained_checkpoints_asset_root_dir(): - raise Exception("A /persistent/isaaclab/asset_root/pretrained_checkpoints setting is required to publish.") - - -def train_job(workflow, task_name, headless=False, force=False, num_envs=None): - """ - This trains a task using the workflow's train.py script, overriding the experiment name to ensure unique - log directories. By default it will return if an experiment has already been run. - - Args: - workflow: The workflow. - task_name: The task name. - headless: Should the training run without the UI. - force: Run training even if previous experiments have been run. - num_envs: How many simultaneous environments to simulate, overriding the config. - """ - - log_root_path = get_log_root_path(workflow, task_name) - - # We already ran this - if not force and os.path.exists(log_root_path) and len(os.listdir(log_root_path)) > 0: - print(f"Skipping training of {workflow}:{task_name}, already has been run") - return - - print(f"Training {workflow}:{task_name}") - - # Construct our command - cmd = [ - sys.executable, - WORKFLOW_TRAINER[workflow], - "--task", - task_name, - "--enable_cameras", - ] - - # Changes the directory name for logging - if WORKFLOW_EXPERIMENT_NAME_VARIABLE[workflow]: - cmd.append(f"{WORKFLOW_EXPERIMENT_NAME_VARIABLE[workflow]}={task_name}") - - if headless: - cmd.append("--headless") - if num_envs: - cmd.extend(["--num_envs", str(num_envs)]) - - print("Running : " + " ".join(cmd)) - - subprocess.run(cmd) - - -def review_pretrained_checkpoint(workflow, task_name, force_review=False, num_envs=None): - """ - This initiates a review of the pretrained checkpoint. The play.py script for the workflow is run, and the user - inspects the results. When done they close the simulator and will be prompted for their review. - - Args: - workflow: The workflow. - task_name: The task name. - force_review: Performs the review even if a review already exists. - num_envs: How many simultaneous environments to simulate, overriding the config. - """ - - # This workflow task pair hasn't been trained - if not has_pretrained_checkpoint_job_run(workflow, task_name): - print(f"Skipping review of {workflow}:{task_name}, hasn't been trained yet") - return - - # Couldn't find the checkpoint - if not has_pretrained_checkpoint_job_finished(workflow, task_name): - print(f"Training not complete for {workflow}:{task_name}") - return - - review = get_pretrained_checkpoint_review(workflow, task_name) - - if not force_review and review and review["reviewed"]: - print(f"Review already complete for {workflow}:{task_name}") - return - - print(f"Reviewing {workflow}:{task_name}") - - # Construct our command - cmd = [ - sys.executable, - WORKFLOW_PLAYER[workflow], - "--task", - task_name, - "--checkpoint", - get_pretrained_checkpoint_path(workflow, task_name), - "--enable_cameras", - ] - - if num_envs: - cmd.extend(["--num_envs", str(num_envs)]) - - print("Running : " + " ".join(cmd)) - - subprocess.run(cmd) - - # Give user a chance to leave the old review - if force_review and review and review["reviewed"]: - result = review["result"] - notes = review.get("notes") - print(f"A review already exists for {workflow}:{task_name}, it was marked as '{result}'.") - print(f" Notes: {notes}") - answer = input("Would you like to replace it? Please answer yes or no (y/n) [n]: ").strip().lower() - if answer != "y": - return - - # Get the verdict from the user - print(f"Do you accept this checkpoint for {workflow}:{task_name}?") - - answer = input("Please answer yes, no or undetermined (y/n/u) [u]: ").strip().lower() - if answer not in {"y", "n", "u"}: - answer = "u" - answer_map = { - "y": "accepted", - "n": "rejected", - "u": "undetermined", - } - - # Create the review dict - review = { - "reviewed": True, - "result": answer_map[answer], - } - - # Maybe add some notes - notes = input("Please add notes or hit enter: ").strip().lower() - if notes: - review["notes"] = notes - - # Save the review JSON file - path = get_pretrained_checkpoint_review_path(workflow, task_name) - if not path: - raise Exception("This shouldn't be possible, something went very wrong.") - - with open(path, "w") as f: - json.dump(review, f, indent=4) - - -def publish_pretrained_checkpoint(workflow, task_name, force_publish=False): - """ - This publishes the pretrained checkpoint to Nucleus using the asset path in the - /persistent/isaaclab/asset_root/pretrained_checkpoints Carb variable. - - Args: - workflow: The workflow. - task_name: The task name. - force_publish: Publish without review. - """ - - # This workflow task pair hasn't been trained - if not has_pretrained_checkpoint_job_run(workflow, task_name): - print(f"Skipping publishing of {workflow}:{task_name}, hasn't been trained yet") - return - - # Couldn't find the checkpoint - if not has_pretrained_checkpoint_job_finished(workflow, task_name): - print(f"Training not complete for {workflow}:{task_name}") - return - - # Get local pretrained checkpoint path - local_path = get_pretrained_checkpoint_path(workflow, task_name) - if not local_path: - raise Exception("This shouldn't be possible, something went very wrong.") - - # Not forcing, need to check review results - if not force_publish: - - # Grab the review if it exists - review = get_pretrained_checkpoint_review(workflow, task_name) - - if not review or not review["reviewed"]: - print(f"Skipping publishing of {workflow}:{task_name}, hasn't been reviewed yet") - return - - result = review["result"] - if result != "accepted": - print(f'Skipping publishing of {workflow}:{task_name}, review result was "{result}"') - return - - print(f"Publishing {workflow}:{task_name}") - - # Copy the file - publish_path = get_pretrained_checkpoint_publish_path(workflow, task_name) - omni.client.copy_file(local_path, publish_path, CopyBehavior.OVERWRITE) - - -def get_job_summary_row(workflow, task_name): - """Returns a single row summary of the job""" - - has_run = has_pretrained_checkpoint_job_run(workflow, task_name) - has_finished = has_pretrained_checkpoint_job_finished(workflow, task_name) - review = get_pretrained_checkpoint_review(workflow, task_name) - - if review: - result = review.get("result", "undetermined") - notes = review.get("notes", "") - else: - result = "" - notes = "" - - return [workflow, task_name, has_run, has_finished, result, notes] - - -def main(): - - # Figure out what workflows and tasks we'll be using - if args.all: - jobs = ["*:*"] - else: - jobs = args.jobs - - if args.list: - print() - print("# Workflow, Task, Ran, Finished, Review, Notes") - - summary_rows = [] - - # Could be implemented more efficiently, but the performance gain would be inconsequential - for workflow in WORKFLOWS: - for task_spec in sorted(gym.registry.values(), key=lambda t: t.id): - job_id = f"{workflow}:{task_spec.id}" - - # We've excluded this job - if any(fnmatch.fnmatch(job_id, e) for e in args.exclude): - continue - - # None of our jobs match this pair - if not np.any(np.array([fnmatch.fnmatch(job_id, job) for job in jobs])): - continue - - # No config for this workflow - if workflow + "_cfg_entry_point" not in task_spec.kwargs: - continue - - if args.list: - summary_rows.append(get_job_summary_row(workflow, task_spec.id)) - continue - - # Training reviewing and publishing - if args.train: - train_job(workflow, task_spec.id, args.headless, args.force, args.num_envs) - - if args.review: - review_pretrained_checkpoint(workflow, task_spec.id, args.force_review, args.num_envs) - - if args.publish_checkpoint: - publish_pretrained_checkpoint(workflow, task_spec.id, args.force_publish) - - if args.list: - writer = csv.writer(sys.stdout, quotechar='"', quoting=csv.QUOTE_MINIMAL) - writer.writerows(summary_rows) - - -if __name__ == "__main__": - - try: - # Run the main function - main() - except Exception as e: - raise e - finally: - # Close the app - simulation_app.close() diff --git a/scripts/tools/process_meshes_to_obj.py b/scripts/tools/process_meshes_to_obj.py index 331f7b09..2c5be04c 100644 --- a/scripts/tools/process_meshes_to_obj.py +++ b/scripts/tools/process_meshes_to_obj.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/scripts/tools/record_demos.py b/scripts/tools/record_demos.py index e3609d39..51b09132 100644 --- a/scripts/tools/record_demos.py +++ b/scripts/tools/record_demos.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """ @@ -9,26 +9,48 @@ 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. +This script supports two teleoperation stacks: +1. Native Isaac Lab teleop stack (via teleop_devices in env_cfg) +2. IsaacTeleop-based stack (via isaac_teleop in env_cfg) + +The script automatically detects which stack to use based on the environment config. + 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) + --teleop_device Legacy teleop device name. When omitted, IsaacTeleop is used if + configured, otherwise keyboard. When set, forces the legacy path. --dataset_file File path to export recorded demos. (default: "./datasets/dataset.hdf5") --step_hz Environment stepping rate in Hz. (default: 30) --num_demos Number of demonstrations to record. (default: 0) - --num_success_steps Number of continuous steps with task success for concluding a demo as successful. (default: 10) + --num_success_steps Number of continuous steps with task success for concluding a demo as successful. + (default: 10) """ """Launch Isaac Sim Simulator first.""" +# Isaac Lab does not use Warp autodiff; skipping adjoint codegen roughly halves the +# time spent building kernels on a cold kernel cache. +import warp as wp + +wp.config.enable_backward = False + # Standard library imports import argparse import contextlib +import sys +from typing import TYPE_CHECKING # Isaac Lab AppLauncher from isaaclab.app import AppLauncher +from isaaclab.utils.string import list_intersection, string_to_callable + +from isaaclab_tasks.utils import setup_preset_cli + +if TYPE_CHECKING: + from isaaclab_teleop import XrCameraFeedSession # add argparse arguments parser = argparse.ArgumentParser(description="Record demonstrations for Isaac Lab environments.") @@ -36,11 +58,11 @@ parser.add_argument( "--teleop_device", type=str, - default="keyboard", + default=None, help=( - "Teleop device. Set here (legacy) or via the environment config. If using the environment config, pass the" - " device key/name defined under 'teleop_devices' (it can be a custom name, not necessarily 'handtracking')." - " Built-ins: keyboard, spacemouse, gamepad. Not all tasks support all built-ins." + "Legacy teleop device name. When omitted, the IsaacTeleop pipeline is used if configured in the env," + " otherwise keyboard is used as fallback. When explicitly provided, the script uses the legacy" + " teleop_devices path and looks up this name in env_cfg.teleop_devices.devices." ), ) parser.add_argument( @@ -57,16 +79,64 @@ help="Number of continuous steps with task success for concluding a demo as successful. Default is 10.", ) parser.add_argument( - "--enable_pinocchio", + "--reset_sim_buffer_each_episode", + action=argparse.BooleanOptionalAction, + default=True, + help=( + "Call env.sim.reset() before the initial episode and between recording attempts." + " Use --no-reset_sim_buffer_each_episode to preserve simulation buffers." + ), +) +parser.add_argument( + "--cloudxr_env", + type=str, + default=None, + help=( + "Path to a CloudXR .env file, or a shorthand: 'cloudxrjs' (Quest/Pico), 'avp' (Apple Vision Pro)," + " or 'standalone' (headless, no XR client). Set to 'none' to disable CloudXR auto-launch entirely." + " When unset, defaults to 'cloudxrjs' with --xr and 'standalone' without --xr." + ), +) +parser.add_argument( + "--auto_launch_cloudxr", + action=argparse.BooleanOptionalAction, + default=True, + help="Auto-launch the CloudXR runtime when --cloudxr_env is set. Use --no-auto_launch_cloudxr to disable.", +) +parser.add_argument( + "--mcap_record_path", + type=str, + default=None, + help=( + "Debug-only: write the live IsaacTeleop session to this MCAP file (one continuous file for the whole run)." + " Intended for pairing with teleop_replay_agent.py in CI -- NOT a data-generation format. MCAPs produced" + " here lack per-episode segmentation, world-frame anchor state, env reset state, and have no public Python" + " decoder. For data-gen workflows use the HDF5 dataset path (default). Ignored when the IsaacTeleop stack" + " is not in use." + ), +) + +parser.add_argument( + "--enable_debug_visualization", action="store_true", default=False, - help="Enable Pinocchio.", + help="Enable hand joint and controller aim debug visualization at session start (IsaacTeleop only).", +) +parser.add_argument("--external_callback", default=None, help="Fully qualified path to an externally defined callback.") +parser.add_argument( + "--disable_external_cameras", + action="store_true", + default=False, + help=( + "Disable external camera rendering. External cameras render by default for teleoperation;" + " pass this flag to strip camera sensors from the environment (e.g. to reduce GPU contention" + " and improve XR performance)." + ), ) - # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments -args_cli = parser.parse_args() +args_cli, hydra_args = setup_preset_cli(parser) # Validate required arguments if args_cli.task is None: @@ -74,54 +144,119 @@ app_launcher_args = vars(args_cli) -if args_cli.enable_pinocchio: - # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim - # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter - import pinocchio # noqa: F401 -if "handtracking" in args_cli.teleop_device.lower(): - app_launcher_args["xr"] = True - # launch the simulator -app_launcher = AppLauncher(args_cli) +# Enable external camera rendering by default (``--disable_external_cameras`` turns it off). The +# ``--enable_cameras`` CLI flag was removed in Isaac Lab 3.0 (see #6656), so pass the intent to +# AppLauncher as a kwarg; this selects a camera-rendering experience that provides RTX/DLSS support. +# Everywhere else we read ``args_cli.disable_external_cameras`` directly. +app_launcher = AppLauncher(args_cli, enable_cameras=not args_cli.disable_external_cameras) simulation_app = app_launcher.app +# Call an external callback if requested. +remaining_args_env_registration = None +if args_cli.external_callback: + external_callback_function = string_to_callable(args_cli.external_callback, separator=".") + remaining_args_env_registration = external_callback_function() + +# Hand arguments consumed by neither this parser nor the callback over to Hydra. +hydra_args = list_intersection(hydra_args, remaining_args_env_registration) +sys.argv = [sys.argv[0]] + hydra_args + """Rest everything follows.""" # Third-party imports -import gymnasium as gym import logging import os import time +from collections.abc import Callable + +import gymnasium as gym import torch +from isaaclab_physx.renderers import IsaacRtxRendererGlobalSettingsCfg +from isaaclab_physx.renderers.isaac_rtx_renderer_utils import ( + apply_isaac_rtx_global_settings, +) import omni.ui as ui from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg, Se3SpaceMouse, Se3SpaceMouseCfg from isaaclab.devices.openxr import remove_camera_configs from isaaclab.devices.teleop_device_factory import create_teleop_device - -import isaaclab_mimic.envs # noqa: F401 -from isaaclab_mimic.ui.instruction_display import InstructionDisplay, show_subtask_instructions - -if args_cli.enable_pinocchio: - import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 - import isaaclab_tasks.manager_based.locomanipulation.pick_place # noqa: F401 - -from collections.abc import Callable - from isaaclab.envs import DirectRLEnvCfg, ManagerBasedRLEnvCfg from isaaclab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg from isaaclab.envs.ui import EmptyWindow from isaaclab.managers import DatasetExportMode +import isaaclab_mimic.envs # noqa: F401 +from isaaclab_mimic.ui.instruction_display import InstructionDisplay, show_subtask_instructions + import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -from isaaclab_tasks.utils.parse_cfg import parse_env_cfg +from isaaclab_tasks.utils import resolve_task_config +from isaaclab_tasks.utils.parse_cfg import load_cfg_from_registry -# import logger logger = logging.getLogger(__name__) +_CLOUDXR_ENV_SHORTHANDS: dict[str, str] = {} + + +def _never_terminate(env: gym.Env) -> torch.Tensor: + """Return a false termination signal for every environment.""" + return torch.zeros(env.num_envs, dtype=torch.bool, device=env.device) + + +def _resolve_cloudxr_env(value: str | None, xr_enabled: bool = False) -> str | None: + """Resolve ``--cloudxr_env`` shorthands to absolute ``.env`` file paths. + + Accepts ``"cloudxrjs"`` (Quest/Pico), ``"avp"`` (Apple Vision Pro), + ``"standalone"`` (headless, no XR client), ``"none"`` (disable), or an + arbitrary file path. When *value* is ``None`` (flag unset), defaults to + ``"cloudxrjs"`` when *xr_enabled* else ``"standalone"`` -- so a run without + ``--xr`` uses the clientless headless profile. + """ + if value is None: + value = "cloudxrjs" if xr_enabled else "standalone" + if value.strip() == "" or value.lower() == "none": + return None + if not _CLOUDXR_ENV_SHORTHANDS: + from isaaclab_teleop import CLOUDXR_AVP_ENV, CLOUDXR_JS_ENV, CLOUDXR_STANDALONE_ENV + + _CLOUDXR_ENV_SHORTHANDS["cloudxrjs"] = CLOUDXR_JS_ENV + _CLOUDXR_ENV_SHORTHANDS["avp"] = CLOUDXR_AVP_ENV + _CLOUDXR_ENV_SHORTHANDS["standalone"] = CLOUDXR_STANDALONE_ENV + return _CLOUDXR_ENV_SHORTHANDS.get(value.lower(), value) + + +def _rtx_rendering_requested(args: argparse.Namespace) -> bool: + """Return whether the CLI selects a renderer that actually drives RTX rendering. + + The RTX/DLSS global settings are only meaningful when something renders through RTX. + That happens when the Kit visualizer is enabled (``--viz kit``), when external cameras + are rendered (on by default; see ``--disable_external_cameras``), or in XR mode (``--xr``). + A pure-headless session with none of these renders nothing. + + This reads the resolved namespace intent rather than any Kit/carb runtime state so the + check keeps working as these scripts grow support for other renderers and kitless runs. + """ + visualizers = getattr(args, "visualizer", None) or [] + external_cameras = not getattr(args, "disable_external_cameras", False) + return external_cameras or ("kit" in visualizers) or bool(getattr(args, "xr", False)) + + +def _ensure_replicator_loaded() -> None: + """Enable ``omni.replicator.core`` so RTX/DLSS global settings can be applied. + + :func:`apply_isaac_rtx_global_settings` sets the antialiasing mode through + ``omni.replicator.core``, which ships with the SDG/rendering extensions. Some Kit + experiences (e.g. the Kit-viewport-only app selected by ``--visualizer kit`` without + cameras or XR) do not preload it, so enable it on demand via the extension manager + before applying RTX settings. Idempotent when the extension is already enabled. + """ + import omni.kit.app + + omni.kit.app.get_app().get_extension_manager().set_extension_enabled_immediate("omni.replicator.core", True) + class RateLimiter: """Convenience class for enforcing rates in loops.""" @@ -181,7 +316,7 @@ def setup_output_directories() -> tuple[str, str]: def create_environment_config( output_dir: str, output_file_name: str -) -> tuple[ManagerBasedRLEnvCfg | DirectRLEnvCfg, object | None]: +) -> tuple[ManagerBasedRLEnvCfg | DirectRLEnvCfg, object | None, bool]: """Create and configure the environment configuration. Parses the environment configuration and makes necessary adjustments for demo recording. @@ -192,49 +327,74 @@ def create_environment_config( output_file_name: Name of the file to store the demonstrations Returns: - tuple[isaaclab_tasks.utils.parse_cfg.EnvCfg, Optional[object]]: A tuple containing: + tuple[isaaclab_tasks.utils.parse_cfg.EnvCfg, Optional[object], bool]: A tuple containing: - env_cfg: The configured environment configuration - success_term: The success termination object or None if not available + - use_isaac_teleop: Whether IsaacTeleop stack should be used Raises: Exception: If parsing the environment configuration fails """ - # parse configuration + # Resolve the task configuration through Hydra so CLI presets are applied. try: - env_cfg = parse_env_cfg(args_cli.task, device=args_cli.device, num_envs=1) + env_cfg, _ = resolve_task_config(args_cli.task, "") + env_cfg.sim.device = args_cli.device + env_cfg.scene.num_envs = 1 env_cfg.env_name = args_cli.task.split(":")[-1] except Exception as e: logger.error(f"Failed to parse environment configuration: {e}") exit(1) - # extract success checking function to invoke in the main loop - success_term = None - if hasattr(env_cfg.terminations, "success"): - success_term = env_cfg.terminations.success - env_cfg.terminations.success = None + # When --teleop_device is explicitly provided, use the legacy teleop_devices path + # even if isaac_teleop is configured. Otherwise prefer isaac_teleop when available. + teleop_device_explicitly_set = args_cli.teleop_device is not None + use_isaac_teleop = ( + not teleop_device_explicitly_set and hasattr(env_cfg, "isaac_teleop") and env_cfg.isaac_teleop is not None + ) + + # Extract the success condition for manual evaluation in the main loop. Keep + # an inert term registered under the same name so rewards and other manager + # terms that reference "success" can still resolve it during initialization. + success_term = getattr(env_cfg.terminations, "success", None) + if success_term is not None: + env_cfg.terminations.success = success_term.replace(func=_never_terminate, params={}) else: logger.warning( "No success termination term was found in the environment." " Will not be able to mark recorded demos as successful." ) + # XR-rendering setup is only needed for the Kit XR path. Without --xr, + # IsaacTeleop runs standalone (I/O only) and renders normally. if args_cli.xr: - # If cameras are not enabled and XR is enabled, remove camera configs - if not args_cli.enable_cameras: + # Strip camera configs only when external cameras are explicitly disabled; otherwise keep + # them (defaulted on) so cameras render alongside the XR view. + if args_cli.disable_external_cameras: env_cfg = remove_camera_configs(env_cfg) - env_cfg.sim.render.antialiasing_mode = "DLSS" + # Apply the RTX/DLSS global settings when an RTX render pipeline will run (Kit visualizer, + # external cameras, or XR). ``apply_isaac_rtx_global_settings`` uses ``omni.replicator``, + # which some experiences do not preload, so ensure it is loaded first. + if _rtx_rendering_requested(args_cli): + _ensure_replicator_loaded() + apply_isaac_rtx_global_settings( + IsaacRtxRendererGlobalSettingsCfg(antialiasing_mode="DLSS"), + ) # modify configuration such that the environment runs indefinitely until # the goal is reached or other termination conditions are met env_cfg.terminations.time_out = None env_cfg.observations.policy.concatenate_terms = False - env_cfg.recorders: ActionStateRecorderManagerCfg = ActionStateRecorderManagerCfg() + demo_recorder_cfg_entry_point = gym.spec(args_cli.task.split(":")[-1]).kwargs.get("demo_recorder_cfg_entry_point") + if demo_recorder_cfg_entry_point is None: + env_cfg.recorders = ActionStateRecorderManagerCfg() + else: + env_cfg.recorders = load_cfg_from_registry(args_cli.task, "demo_recorder_cfg_entry_point") env_cfg.recorders.dataset_export_dir_path = output_dir env_cfg.recorders.dataset_filename = output_file_name env_cfg.recorders.dataset_export_mode = DatasetExportMode.EXPORT_SUCCEEDED_ONLY - return env_cfg, success_term + return env_cfg, success_term, use_isaac_teleop def create_environment(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg) -> gym.Env: @@ -242,7 +402,7 @@ def create_environment(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg) -> gym.En Args: env_cfg: The environment configuration object that defines the environment properties. - This should be an instance of EnvCfg created by parse_env_cfg(). + This should be an instance of EnvCfg created by resolve_task_config(). Returns: gym.Env: A Gymnasium environment instance for the specified task. @@ -258,7 +418,17 @@ def create_environment(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg) -> gym.En exit(1) -def setup_teleop_device(callbacks: dict[str, Callable]) -> object: +def _create_builtin_device(device_name: str) -> object | None: + """Create a built-in teleop device by name, or return None if unrecognized.""" + name = device_name.lower() + if name == "keyboard": + return Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.2, rot_sensitivity=0.5)) + elif name == "spacemouse": + return Se3SpaceMouse(Se3SpaceMouseCfg(pos_sensitivity=0.2, rot_sensitivity=0.5)) + return None + + +def setup_teleop_device(callbacks: dict[str, Callable], use_isaac_teleop: bool = False) -> object: """Set up the teleoperation device based on configuration. Attempts to create a teleoperation device based on the environment configuration. @@ -267,6 +437,7 @@ def setup_teleop_device(callbacks: dict[str, Callable]) -> object: Args: callbacks: Dictionary mapping callback keys to functions that will be attached to the teleop device + use_isaac_teleop: Whether to use IsaacTeleop stack instead of native stack Returns: object: The configured teleoperation device interface @@ -274,25 +445,46 @@ def setup_teleop_device(callbacks: dict[str, Callable]) -> object: Raises: Exception: If teleop device creation fails """ + teleop_device_explicitly_set = args_cli.teleop_device is not None teleop_interface = None try: - if hasattr(env_cfg, "teleop_devices") and args_cli.teleop_device in env_cfg.teleop_devices.devices: - teleop_interface = create_teleop_device(args_cli.teleop_device, env_cfg.teleop_devices.devices, callbacks) - else: - logger.warning( - f"No teleop device '{args_cli.teleop_device}' found in environment config. Creating default." + if use_isaac_teleop: + from isaaclab_teleop import create_isaac_teleop_device + + teleop_interface = create_isaac_teleop_device( + env_cfg.isaac_teleop, + sim_device=args_cli.device, + callbacks=callbacks, + cloudxr_env_file=_resolve_cloudxr_env(args_cli.cloudxr_env, args_cli.xr), + auto_launch_cloudxr=args_cli.auto_launch_cloudxr, + use_kit_xr_bridge=args_cli.xr, + mcap_record_path=args_cli.mcap_record_path, + enable_debug_visualization=args_cli.enable_debug_visualization, + haptic_cfg=getattr(env_cfg, "haptic_feedback", None), ) - # Create fallback teleop device - if args_cli.teleop_device.lower() == "keyboard": - teleop_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.2, rot_sensitivity=0.5)) - elif args_cli.teleop_device.lower() == "spacemouse": - teleop_interface = Se3SpaceMouse(Se3SpaceMouseCfg(pos_sensitivity=0.2, rot_sensitivity=0.5)) - else: - logger.error(f"Unsupported teleop device: {args_cli.teleop_device}") - logger.error("Supported devices: keyboard, spacemouse, handtracking") - exit(1) + if args_cli.mcap_record_path is not None: + logger.info("Recording live IsaacTeleop session to MCAP (debug-only): %s", args_cli.mcap_record_path) - # Add callbacks to fallback device + elif teleop_device_explicitly_set: + device_name = args_cli.teleop_device + if hasattr(env_cfg, "teleop_devices") and device_name in env_cfg.teleop_devices.devices: + teleop_interface = create_teleop_device(device_name, env_cfg.teleop_devices.devices, callbacks) + else: + teleop_interface = _create_builtin_device(device_name) + if teleop_interface is None: + logger.error( + f"--teleop_device={device_name} was passed but no matching entry exists in" + " env_cfg.teleop_devices and it is not a built-in device name. Either remove" + " --teleop_device to use the IsaacTeleop pipeline, or add a" + f" '{device_name}' entry under teleop_devices in the environment config." + " Built-in devices: keyboard, spacemouse." + ) + exit(1) + for key, callback in callbacks.items(): + teleop_interface.add_callback(key, callback) + else: + # No --teleop_device and no isaac_teleop: fall back to keyboard + teleop_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.2, rot_sensitivity=0.5)) for key, callback in callbacks.items(): teleop_interface.add_callback(key, callback) except Exception as e: @@ -366,36 +558,47 @@ def process_success_condition(env: gym.Env, success_term: object | None, success def handle_reset( - env: gym.Env, success_step_count: int, instruction_display: InstructionDisplay, label_text: str + env: gym.Env, + success_step_count: int, + instruction_display: InstructionDisplay, + label_text: str, + teleop_interface: object | None = None, ) -> int: """Handle resetting the environment. - Resets the environment, recorder manager, and related state variables. - Updates the instruction display with current status. + Resets the environment, recorder manager, teleop device, and related + state variables. Updates the instruction display with current status. Args: - env: The environment instance to reset - success_step_count: Current count of consecutive successful steps - instruction_display: The display object to update - label_text: Text to display showing current recording status + env: The environment instance to reset. + success_step_count: Current count of consecutive successful steps. + instruction_display: The display object to update. + label_text: Text to display showing current recording status. + teleop_interface: Optional teleop device to reset (resets XR anchor + and retargeter cross-step state). Returns: - int: Reset success step count (0) + Reset success step count (0). """ print("Resetting environment...") - env.sim.reset() + if args_cli.reset_sim_buffer_each_episode: + env.sim.reset() env.recorder_manager.reset() env.reset() + if teleop_interface is not None and hasattr(teleop_interface, "reset"): + teleop_interface.reset() success_step_count = 0 instruction_display.show_demo(label_text) return success_step_count -def run_simulation_loop( +def run_simulation_loop( # noqa: C901 env: gym.Env, teleop_interface: object | None, success_term: object | None, rate_limiter: RateLimiter | None, + camera_feed_session: "XrCameraFeedSession", + use_isaac_teleop: bool = False, ) -> int: """Run the main simulation loop for collecting demonstrations. @@ -408,6 +611,8 @@ def run_simulation_loop( teleop_interface: Optional teleop interface (will be created if None) success_term: The success termination object or None if not available rate_limiter: Optional rate limiter to control simulation speed + camera_feed_session: Shared XR camera-feed lifecycle + use_isaac_teleop: Whether to use IsaacTeleop stack Returns: int: Number of successful demonstrations recorded @@ -415,11 +620,21 @@ def run_simulation_loop( current_recorded_demo_count = 0 success_step_count = 0 should_reset_recording_instance = False - running_recording_instance = not args_cli.xr + # For IsaacTeleop or XR, default to inactive until START is triggered. Without + # --xr, recording is started locally (see ``request_start`` below) instead of by + # a headset; it flows through the same state machine so keyboard/host pause/resume + # keeps working. + running_recording_instance = not (args_cli.xr or use_isaac_teleop) # Callback closures for the teleop device def reset_recording_instance(): nonlocal should_reset_recording_instance + if success_step_count > 0: + print( + "Manual reset ignored. Success has fired and post-success steps are still recording. Please wait for" + " the automatic reset." + ) + return should_reset_recording_instance = True print("Recording instance reset requested") @@ -433,7 +648,9 @@ def stop_recording_instance(): running_recording_instance = False print("Recording paused") - # Set up teleoperation callbacks + # Set up teleoperation callbacks. For IsaacTeleop the primary control + # path is poll_control_events(); these callbacks are bridged automatically + # and also serve native (keyboard / spacemouse) devices. teleoperation_callbacks = { "R": reset_recording_instance, "START": start_recording_instance, @@ -441,74 +658,153 @@ def stop_recording_instance(): "RESET": reset_recording_instance, } - teleop_interface = setup_teleop_device(teleoperation_callbacks) - teleop_interface.add_callback("R", reset_recording_instance) - - # Reset before starting - env.sim.reset() - env.reset() - teleop_interface.reset() + teleop_interface = setup_teleop_device(teleoperation_callbacks, use_isaac_teleop) + + # Optional controller haptics: no-ops unless the env declares a + # ``haptic_feedback`` config and the device can render it (IsaacTeleop). + # ``haptic_update`` renders the current contact force; ``haptic_stop`` zeroes + # it so a stale pulse does not persist while recording is paused. + haptic_update, haptic_stop = (lambda: None), (lambda: None) + if use_isaac_teleop: + from isaaclab_teleop import create_haptic_feedback_driver + + _haptic_driver = create_haptic_feedback_driver(env.unwrapped, teleop_interface, env_cfg) + if _haptic_driver is not None: + haptic_update, haptic_stop = _haptic_driver.update, _haptic_driver.stop + + # Optional keyboard for headset-free IsaacTeleop control (start / pause / reset). + # Captured through the app window, so only wired when one is present; a + # windowless run still auto-starts in ``inner_loop``. Kept in a local so its carb + # input subscription is not garbage-collected. ``R`` is an operator reset: + # ``reset(pause=True)`` injects a single RESET pulse (the control-event handler + # turns it into one env reset) and pauses the session -- binding it straight to + # ``reset_recording_instance`` would reset the env twice. + control_keyboard = None + if use_isaac_teleop and app_launcher.has_window: + try: + control_keyboard = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.0, rot_sensitivity=0.0)) + control_keyboard.add_callback("B", teleop_interface.request_start) + control_keyboard.add_callback("P", teleop_interface.request_stop) + control_keyboard.add_callback("R", lambda: teleop_interface.reset(pause=True)) + print("IsaacTeleop control keys: [B] start/resume [P] pause [R] reset") + except Exception as e: + logger.warning(f"Control keyboard unavailable ({e}); recording still auto-starts without --xr") + control_keyboard = None label_text = f"Recorded {current_recorded_demo_count} successful demonstrations." instruction_display = setup_ui(label_text, env) - subtasks = {} - - with contextlib.suppress(KeyboardInterrupt) and torch.inference_mode(): - while simulation_app.is_running(): - # Get keyboard command - action = teleop_interface.advance() - # Expand to batch dimension - actions = action.repeat(env.num_envs, 1) - - # Perform action on environment - if running_recording_instance: - # Compute actions based on environment - obv = env.step(actions) - if subtasks is not None: - if subtasks == {}: - subtasks = obv[0].get("subtask_terms") - elif subtasks: - show_subtask_instructions(instruction_display, subtasks, obv, env.cfg) - else: - env.sim.render() - - # Check for success condition - success_step_count, success_reset_needed = process_success_condition(env, success_term, success_step_count) - if success_reset_needed: - should_reset_recording_instance = True - - # Update demo count if it has changed - if env.recorder_manager.exported_successful_episode_count > current_recorded_demo_count: - current_recorded_demo_count = env.recorder_manager.exported_successful_episode_count - label_text = f"Recorded {current_recorded_demo_count} successful demonstrations." - print(label_text) - - # Check if we've reached the desired number of demos - if args_cli.num_demos > 0 and env.recorder_manager.exported_successful_episode_count >= args_cli.num_demos: - label_text = f"All {current_recorded_demo_count} demonstrations recorded.\nExiting the app." - instruction_display.show_demo(label_text) - print(label_text) - target_time = time.time() + 0.8 - while time.time() < target_time: - if rate_limiter: - rate_limiter.sleep(env) - else: - env.sim.render() - break - - # Handle reset if requested - if should_reset_recording_instance: - success_step_count = handle_reset(env, success_step_count, instruction_display, label_text) - should_reset_recording_instance = False - - # Check if simulation is stopped - if env.sim.is_stopped(): - break - - # Rate limiting - if rate_limiter: - rate_limiter.sleep(env) + def inner_loop(): + """Inner loop function with access to nonlocal variables.""" + nonlocal current_recorded_demo_count, success_step_count, should_reset_recording_instance + nonlocal running_recording_instance, label_text + + # Reset before starting + if args_cli.reset_sim_buffer_each_episode: + env.sim.reset() + env.reset() + teleop_interface.reset() + + # Without --xr there is no headset to send START, so drive the IsaacTeleop + # state machine to RUNNING locally ([B]/[P] can still pause/resume). The reset() + # above is a host reset (a pure pulse), so it does not cancel this start. + if use_isaac_teleop and not args_cli.xr: + teleop_interface.request_start() + + subtasks = {} + stack_name = "IsaacTeleop" if use_isaac_teleop else "native" + print(f"{stack_name} recording started.") + + if use_isaac_teleop: + from isaaclab_teleop import poll_control_events + + with contextlib.suppress(KeyboardInterrupt), torch.inference_mode(), camera_feed_session.bind(env): + while simulation_app.is_running(): + # Get teleop command (may be None while waiting for session start) + action = teleop_interface.advance() + + if use_isaac_teleop: + ctrl = poll_control_events(teleop_interface) + if ctrl.is_active is not None: + running_recording_instance = ctrl.is_active + if ctrl.should_reset: + should_reset_recording_instance = True + + if action is None: + env.sim.render() + haptic_stop() + continue + # Expand to batch dimension + actions = action.repeat(env.num_envs, 1) + + # Perform action on environment + if running_recording_instance: + # Compute actions based on environment + obv = env.step(actions) + # render controller haptics from post-step contact forces + haptic_update() + if subtasks is not None: + if subtasks == {}: + subtasks = obv[0].get("subtask_terms") + elif subtasks: + show_subtask_instructions(instruction_display, subtasks, obv, env.cfg) + else: + env.sim.render() + # not stepping: zero haptics so a paused grip stops buzzing + haptic_stop() + + # Check for success condition + success_step_count_new, success_reset_needed = process_success_condition( + env, success_term, success_step_count + ) + success_step_count = success_step_count_new + if success_reset_needed: + should_reset_recording_instance = True + + # Update demo count if it has changed + if env.recorder_manager.exported_successful_episode_count > current_recorded_demo_count: + current_recorded_demo_count = env.recorder_manager.exported_successful_episode_count + label_text = f"Recorded {current_recorded_demo_count} successful demonstrations." + print(label_text) + + # Check if we've reached the desired number of demos + if ( + args_cli.num_demos > 0 + and env.recorder_manager.exported_successful_episode_count >= args_cli.num_demos + ): + label_text = f"All {current_recorded_demo_count} demonstrations recorded.\nExiting the app." + instruction_display.show_demo(label_text) + print(label_text) + target_time = time.time() + 0.8 + while time.time() < target_time: + if rate_limiter: + rate_limiter.sleep(env) + else: + env.sim.render() + break + + # Handle reset if requested + if should_reset_recording_instance: + success_step_count = handle_reset( + env, success_step_count, instruction_display, label_text, teleop_interface + ) + camera_feed_session.refresh() + should_reset_recording_instance = False + + # Check if simulation is stopped + if env.sim.is_stopped(): + break + + # Rate limiting + if rate_limiter: + rate_limiter.sleep(env) + + # Run the loop with or without context manager based on stack + if use_isaac_teleop: + with teleop_interface: + inner_loop() + else: + inner_loop() return current_recorded_demo_count @@ -526,7 +822,30 @@ def main() -> None: Raises: Exception: Propagates exceptions from any of the called functions """ - # if handtracking is selected, rate limiting is achieved via OpenXR + # Set up output directories + output_dir, output_file_name = setup_output_directories() + + # Create and configure environment + global env_cfg # Make env_cfg available to setup_teleop_device + env_cfg, success_term, use_isaac_teleop = create_environment_config(output_dir, output_file_name) + + from isaaclab_teleop import XrCameraFeedSession + + camera_feed_session = XrCameraFeedSession.prepare( + env_cfg, + enabled=args_cli.xr and use_isaac_teleop, + camera_rendering_enabled=not args_cli.disable_external_cameras, + ) + if camera_feed_session.requires_responsive_denoising: + apply_isaac_rtx_global_settings( + IsaacRtxRendererGlobalSettingsCfg( + carb_settings={"/rtx/dldenoiser/responsiveDenoising": True}, + ) + ) + + # With --xr, rate limiting is achieved via OpenXR and the XR visualization + # manager is installed. Without --xr (including standalone IsaacTeleop I/O), + # fall back to the software rate limiter and skip the XR viz stack. if args_cli.xr: rate_limiter = None from isaaclab.ui.xr_widgets import TeleopVisualizationManager, XRVisualization @@ -536,18 +855,13 @@ def main() -> None: else: rate_limiter = RateLimiter(args_cli.step_hz) - # Set up output directories - output_dir, output_file_name = setup_output_directories() - - # Create and configure environment - global env_cfg # Make env_cfg available to setup_teleop_device - env_cfg, success_term = create_environment_config(output_dir, output_file_name) - # Create environment env = create_environment(env_cfg) # Run simulation loop - current_recorded_demo_count = run_simulation_loop(env, None, success_term, rate_limiter) + current_recorded_demo_count = run_simulation_loop( + env, None, success_term, rate_limiter, camera_feed_session, use_isaac_teleop + ) # Clean up env.close() @@ -558,5 +872,7 @@ def main() -> None: if __name__ == "__main__": # run the main function main() - # close sim app + # env.close() already closes the USD stage via sim.clear_instance(). + # Pump the event loop so the viewport processes closure, then close the app. + simulation_app.update() simulation_app.close() diff --git a/scripts/tools/replay_demos.py b/scripts/tools/replay_demos.py index e3d776fa..ae36414c 100644 --- a/scripts/tools/replay_demos.py +++ b/scripts/tools/replay_demos.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """Script to replay demonstrations with Isaac Lab environments.""" @@ -7,9 +7,19 @@ """Launch Isaac Sim Simulator first.""" +# Isaac Lab does not use Warp autodiff; skipping adjoint codegen roughly halves the +# time spent building kernels on a cold kernel cache. +import warp as wp + +wp.config.enable_backward = False + import argparse +import sys from isaaclab.app import AppLauncher +from isaaclab.utils.string import list_intersection, string_to_callable + +from isaaclab_tasks.utils import setup_preset_cli # add argparse arguments parser = argparse.ArgumentParser(description="Replay demonstrations in Isaac Lab environments.") @@ -33,44 +43,56 @@ ), ) parser.add_argument( - "--enable_pinocchio", + "--validate_success_rate", action="store_true", default=False, - help="Enable Pinocchio.", + help="Validate the replay success rate using the task environment termination criteria", +) +parser.add_argument( + "--reset_sim_buffer_each_episode", + action="store_true", + default=False, + help=( + "Before loading each episode's initial state, call env.sim.reset() to clear" + " simulation buffers. Only valid with --num_envs 1." + ), ) +parser.add_argument("--external_callback", default=None, help="Fully qualified path to an externally defined callback.") # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments -args_cli = parser.parse_args() +args_cli, hydra_args = setup_preset_cli(parser) # args_cli.headless = True -if args_cli.enable_pinocchio: - # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim - # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter - import pinocchio # noqa: F401 - # launch the simulator app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app +# Call an external callback if requested. +remaining_args_env_registration = None +if args_cli.external_callback: + external_callback_function = string_to_callable(args_cli.external_callback, separator=".") + remaining_args_env_registration = external_callback_function() + +# Hand arguments consumed by neither this parser nor the callback over to Hydra. +hydra_args = list_intersection(hydra_args, remaining_args_env_registration) +sys.argv = [sys.argv[0]] + hydra_args + """Rest everything follows.""" import contextlib -import gymnasium as gym import os + +import gymnasium as gym import torch from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg from isaaclab.utils.datasets import EpisodeData, HDF5DatasetFileHandler -if args_cli.enable_pinocchio: - import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 - import isaaclab_tasks.manager_based.locomanipulation.pick_place # noqa: F401 - import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -from isaaclab_tasks.utils.parse_cfg import parse_env_cfg +from isaaclab_tasks.utils import resolve_task_config is_paused = False @@ -115,95 +137,84 @@ def compare_states(state_from_dataset, runtime_state, runtime_env_index) -> (boo 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.split(":")[-1] - 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(args_cli.task, cfg=env_cfg).unwrapped - - teleop_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.1, rot_sensitivity=0.1)) - teleop_interface.add_callback("N", play_cb) - teleop_interface.add_callback("B", pause_cb) - print('Press "B" to pause and "N" to resume the replayed actions.') - - # 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.") - - # Get idle action (idle actions are applied to envs without next action) - if hasattr(env_cfg, "idle_action"): - idle_action = env_cfg.idle_action.repeat(num_envs, 1) - else: - idle_action = torch.zeros(env.action_space.shape) - - # reset before starting - env.reset() - teleop_interface.reset() +def replay_episodes_loop( # noqa: C901 + env, + dataset_file_handler: HDF5DatasetFileHandler, + episode_names: list[str], + episode_count: int, + episode_indices_to_replay: list[int], + num_envs: int, + success_term, + state_validation_enabled: bool, + idle_action: torch.Tensor, + reset_sim_buffer_each_episode: bool, +) -> tuple[int, int, list[int]]: + """Run the replay loop until all selected episodes finish or the app exits. - # simulate environment -- run everything in inference mode - episode_names = list(dataset_file_handler.get_episode_names()) + Returns: + Tuple of (replayed_episode_count, recorded_episode_count, failed_demo_ids). + """ replayed_episode_count = 0 + recorded_episode_count = 0 + current_episode_indices: list[int | None] = [None] * num_envs + failed_demo_ids: list[int] = [] + 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 + episode_ended = [False] * num_envs while has_next_action: # initialize actions with idle action so those without next action will not move - actions = idle_action + actions = idle_action.clone() 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: + # check if the episode is successful after the whole episode_data is + if ( + (success_term is not None) + and (current_episode_indices[env_id]) is not None + and (not episode_ended[env_id]) + ): + if bool(success_term.func(env, **success_term.params)[env_id]): + recorded_episode_count += 1 + plural_trailing_s = "s" if recorded_episode_count > 1 else "" + + print( + f"Successfully replayed {recorded_episode_count} episode{plural_trailing_s} out" + f" of {replayed_episode_count} demos." + ) + else: + # if not successful, add to failed demo IDs list + cid = current_episode_indices[env_id] + if cid is not None and cid not in failed_demo_ids: + failed_demo_ids.append(cid) + + episode_ended[env_id] = True + next_episode_index = None while episode_indices_to_replay: next_episode_index = episode_indices_to_replay.pop(0) + if next_episode_index < episode_count: + episode_ended[env_id] = False 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}") + current_episode_indices[env_id] = next_episode_index + 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.device ) env_episode_data_map[env_id] = episode_data # Set initial state for the new episode initial_state = episode_data.get_initial_state() + if reset_sim_buffer_each_episode: + env.sim.reset() env.reset_to(initial_state, torch.tensor([env_id], device=env.device), is_relative=True) # Get the first action for the new episode env_next_action = env_episode_data_map[env_id].get_next_action() @@ -213,6 +224,9 @@ def main(): else: has_next_action = True actions[env_id] = env_next_action + if not has_next_action: + # Stop before stepping once every environment has exhausted its recorded actions. + break if first_loop: first_loop = False else: @@ -225,7 +239,7 @@ def main(): 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}", + f"Validating states at action-index: {env_episode_data_map[0].next_state_index - 1:4}", end="", ) current_runtime_state = env.scene.get_state(is_relative=True) @@ -236,9 +250,114 @@ def main(): print("\t- mismatched.") print(comparison_log) break + + return replayed_episode_count, recorded_episode_count, failed_demo_ids + + +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 = list(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.split(":")[-1] + if env_name is None: + raise ValueError("Task/env name was not specified nor found in the dataset.") + + num_envs = args_cli.num_envs + if args_cli.reset_sim_buffer_each_episode and num_envs != 1: + raise ValueError( + "--reset_sim_buffer_each_episode is only supported with a single environment (--num_envs 1). " + f"Got num_envs={num_envs}. Use --num_envs 1 or disable --reset_sim_buffer_each_episode." + ) + + env_cfg, _ = resolve_task_config(env_name, "") + env_cfg.sim.device = args_cli.device + env_cfg.scene.num_envs = num_envs + + # extract success checking function to invoke in the main loop + success_term = None + if args_cli.validate_success_rate: + if hasattr(env_cfg.terminations, "success"): + success_term = env_cfg.terminations.success + env_cfg.terminations.success = None + else: + print( + "No success termination term was found in the environment." + " Will not be able to mark recorded demos as successful." + ) + + # Disable all recorders and terminations + env_cfg.recorders = {} + env_cfg.terminations = {} + + # create environment from loaded config + env = gym.make(args_cli.task, cfg=env_cfg).unwrapped + + teleop_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.1, rot_sensitivity=0.1)) + teleop_interface.add_callback("N", play_cb) + teleop_interface.add_callback("B", pause_cb) + print('Press "B" to pause and "N" to resume the replayed actions.') + + # 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.") + + # Get idle action (idle actions are applied to envs without next action) + if hasattr(env_cfg, "idle_action"): + idle_action = torch.tensor(env_cfg.idle_action, device=env.unwrapped.device).repeat(num_envs, 1) + else: + idle_action = torch.zeros(env.action_space.shape) + + # reset before starting + env.reset() + teleop_interface.reset() + + episode_names = list(dataset_file_handler.get_episode_names()) + replayed_episode_count, recorded_episode_count, failed_demo_ids = replay_episodes_loop( + env, + dataset_file_handler, + episode_names, + episode_count, + episode_indices_to_replay, + num_envs, + success_term, + state_validation_enabled, + idle_action, + args_cli.reset_sim_buffer_each_episode, + ) + # 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}.") + + # Print success statistics only if validation was enabled + if success_term is not None: + print(f"Successfully replayed: {recorded_episode_count}/{replayed_episode_count}") + + # Print failed demo IDs if any + if failed_demo_ids: + print(f"\nFailed demo IDs ({len(failed_demo_ids)} total):") + print(f" {sorted(failed_demo_ids)}") + env.close() diff --git a/scripts/tools/test/test_converter_output.py b/scripts/tools/test/test_converter_output.py new file mode 100644 index 00000000..d3b300c8 --- /dev/null +++ b/scripts/tools/test/test_converter_output.py @@ -0,0 +1,120 @@ +# Copyright (c) 2026, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import ast +import contextlib +import os +from pathlib import Path +from types import SimpleNamespace + +import pytest +from pxr import Sdf, Usd, UsdGeom + +from scripts.tools.usd_output import write_usd_entry_layer + +ROOT = Path(__file__).resolve().parents[3] + + +class ConverterCfg: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + def to_dict(self): + return vars(self) + + +ConverterCfg.JointDriveCfg = ConverterCfg +ConverterCfg.PDGainsCfg = ConverterCfg + + +def make_imported_asset(directory): + directory.mkdir(parents=True) + part = Usd.Stage.CreateNew(str(directory / "part.usda")) + prim = UsdGeom.Cube.Define(part, "/Robot/Part") + prim.CreateSizeAttr(2.5) + part.GetRootLayer().Save() + generated = Usd.Stage.CreateNew(str(directory / "robot.usda")) + robot = generated.DefinePrim("/Robot", "Xform") + generated.SetDefaultPrim(robot) + UsdGeom.SetStageUpAxis(generated, "Y") + UsdGeom.SetStageMetersPerUnit(generated, 0.01) + generated.GetRootLayer().subLayerPaths = ["part.usda"] + generated.GetRootLayer().Save() + return directory / "robot.usda" + + +@pytest.mark.parametrize("kind", ["urdf", "mjcf"]) +@pytest.mark.parametrize("extension", ["usd", "usda", "usdc"]) +def test_converter_main_honors_requested_filename(tmp_path, kind, extension): + source = tmp_path / f"input.{kind}" + source.touch() + output = tmp_path / "output" / f"custom name.{extension}" + previews = [] + + def convert(cfg): + path = make_imported_asset(Path(cfg.usd_dir) / "robot") + return SimpleNamespace(usd_path=str(path)) + + args = SimpleNamespace( + input=str(source), + output=str(output), + fix_base=False, + merge_joints=False, + joint_stiffness=100.0, + joint_damping=1.0, + joint_target_type="position", + merge_mesh=False, + collision_from_visuals=False, + collision_type="convexHull", + self_collision=False, + import_physics_scene=False, + ) + namespace = { + "args_cli": args, + "os": os, + "check_file_path": os.path.isfile, + "print_dict": lambda *a, **kw: None, + "PhysicsCfg": object, + "launch_simulation": lambda **kw: contextlib.nullcontext(None), + "preview": lambda path, cfg: previews.append(path), + "write_usd_entry_layer": write_usd_entry_layer, + "UrdfConverterCfg": ConverterCfg, + "MjcfConverterCfg": ConverterCfg, + "UrdfConverter": convert, + "MjcfConverter": convert, + } + tree = ast.parse((ROOT / f"scripts/tools/convert_{kind}.py").read_text()) + main = next(node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "main") + exec(compile(ast.Module(body=[main], type_ignores=[]), str(source), "exec"), namespace) + namespace["main"]() + assert output.is_file() + assert previews == [str(output)] + stage = Usd.Stage.Open(str(output)) + assert stage.GetDefaultPrim().GetPath() == Sdf.Path("/Robot") + assert UsdGeom.GetStageUpAxis(stage) == "Y" + assert UsdGeom.GetStageMetersPerUnit(stage) == 0.01 + assert UsdGeom.Cube(stage.GetPrimAtPath("/Robot/Part")).GetSizeAttr().Get() == 2.5 + assert stage.GetRootLayer().subLayerPaths == ["robot/robot.usda"] + + +def test_same_output_layer_is_not_rewritten(tmp_path): + generated = make_imported_asset(tmp_path / "robot") + before = generated.read_bytes() + assert write_usd_entry_layer(str(generated), str(generated)) == str(generated) + assert generated.read_bytes() == before + + +@pytest.mark.parametrize("symlink", [False, True]) +def test_output_alias_cannot_overwrite_generated_layer(tmp_path, symlink): + generated = make_imported_asset(tmp_path / "robot") + output = tmp_path / "alias.usda" + if symlink: + output.symlink_to(generated) + else: + os.link(generated, output) + before = generated.read_bytes() + with pytest.raises(ValueError, match="aliases"): + write_usd_entry_layer(str(generated), str(output)) + assert generated.read_bytes() == before diff --git a/scripts/tools/test/test_cosmos_prompt_gen.py b/scripts/tools/test/test_cosmos_prompt_gen.py index d644d332..17f1764d 100644 --- a/scripts/tools/test/test_cosmos_prompt_gen.py +++ b/scripts/tools/test/test_cosmos_prompt_gen.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2024-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -17,7 +17,7 @@ @pytest.fixture(scope="class") def temp_templates_file(): """Create temporary templates file.""" - temp_file = tempfile.NamedTemporaryFile(suffix=".json", delete=False) + temp_file = tempfile.NamedTemporaryFile(suffix=".json", delete=False) # noqa: SIM115 # Create test templates test_templates = { @@ -40,7 +40,7 @@ def temp_templates_file(): @pytest.fixture def temp_output_file(): """Create temporary output file.""" - temp_file = tempfile.NamedTemporaryFile(suffix=".txt", delete=False) + temp_file = tempfile.NamedTemporaryFile(suffix=".txt", delete=False) # noqa: SIM115 yield temp_file.name # Cleanup os.remove(temp_file.name) diff --git a/scripts/tools/test/test_hdf5_to_mp4.py b/scripts/tools/test/test_hdf5_to_mp4.py index ef8f97f0..2cb3c777 100644 --- a/scripts/tools/test/test_hdf5_to_mp4.py +++ b/scripts/tools/test/test_hdf5_to_mp4.py @@ -1,15 +1,16 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2024-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """Test cases for HDF5 to MP4 conversion script.""" -import h5py -import numpy as np import os import tempfile +import cv2 +import h5py +import numpy as np import pytest from scripts.tools.hdf5_to_mp4 import get_num_demos, main, write_demo_to_mp4 @@ -18,7 +19,7 @@ @pytest.fixture(scope="class") def temp_hdf5_file(): """Create temporary HDF5 file with test data.""" - temp_file = tempfile.NamedTemporaryFile(suffix=".h5", delete=False) + temp_file = tempfile.NamedTemporaryFile(suffix=".h5", delete=False) # noqa: SIM115 with h5py.File(temp_file.name, "w") as h5f: # Create test data structure for demo_id in range(2): # Create 2 demos @@ -28,6 +29,12 @@ def temp_hdf5_file(): rgb_data = np.random.randint(0, 255, (2, 704, 1280, 3), dtype=np.uint8) demo_group.create_dataset("table_cam", data=rgb_data) + # Create RGB frames stored as floats in [0, 1] + checkerboard = (np.indices((16, 16)).sum(axis=0) % 2).astype(np.float32) + rgb_float_data = np.stack([checkerboard, 1.0 - checkerboard], axis=0) + rgb_float_data = np.repeat(rgb_float_data[..., None], 3, axis=-1) + demo_group.create_dataset("table_cam_float", data=rgb_float_data) + # Create segmentation frames seg_data = np.random.randint(0, 255, (2, 704, 1280, 4), dtype=np.uint8) demo_group.create_dataset("table_cam_segmentation", data=seg_data) @@ -48,7 +55,7 @@ def temp_hdf5_file(): @pytest.fixture def temp_output_dir(): """Create temporary output directory.""" - temp_dir = tempfile.mkdtemp() + temp_dir = tempfile.mkdtemp() # noqa: SIM115 yield temp_dir # Cleanup for file in os.listdir(temp_dir): @@ -72,6 +79,28 @@ def test_write_demo_to_mp4_rgb(self, temp_hdf5_file, temp_output_dir): assert os.path.exists(output_file) assert os.path.getsize(output_file) > 0 + def test_write_demo_to_mp4_float_rgb(self, temp_hdf5_file, temp_output_dir): + """Test writing float RGB frames to MP4.""" + write_demo_to_mp4(temp_hdf5_file, 0, "data/demo_0/obs", "table_cam_float", temp_output_dir, 12, 20) + + output_file = os.path.join(temp_output_dir, "demo_0_table_cam_float.mp4") + assert os.path.exists(output_file) + assert os.path.getsize(output_file) > 0 + + video = cv2.VideoCapture(output_file) + decoded_frames = [] + while True: + success, frame = video.read() + if not success: + break + decoded_frames.append(frame) + video.release() + + assert len(decoded_frames) == 2 + decoded_frames = np.stack(decoded_frames) + assert decoded_frames.shape[1:3] == (12, 20) + assert 60.0 < decoded_frames.mean() < 195.0 + def test_write_demo_to_mp4_segmentation(self, temp_hdf5_file, temp_output_dir): """Test writing segmentation frames to MP4.""" write_demo_to_mp4(temp_hdf5_file, 0, "data/demo_0/obs", "table_cam_segmentation", temp_output_dir, 704, 1280) diff --git a/scripts/tools/test/test_mp4_to_hdf5.py b/scripts/tools/test/test_mp4_to_hdf5.py index f26fb117..631ac41d 100644 --- a/scripts/tools/test/test_mp4_to_hdf5.py +++ b/scripts/tools/test/test_mp4_to_hdf5.py @@ -1,16 +1,16 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2024-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """Test cases for MP4 to HDF5 conversion script.""" -import h5py -import numpy as np import os import tempfile import cv2 +import h5py +import numpy as np import pytest from scripts.tools.mp4_to_hdf5 import get_frames_from_mp4, main, process_video_and_demo @@ -19,7 +19,7 @@ @pytest.fixture(scope="class") def temp_hdf5_file(): """Create temporary HDF5 file with test data.""" - temp_file = tempfile.NamedTemporaryFile(suffix=".h5", delete=False) + temp_file = tempfile.NamedTemporaryFile(suffix=".h5", delete=False) # noqa: SIM115 with h5py.File(temp_file.name, "w") as h5f: # Create test data structure for 2 demos for demo_id in range(2): @@ -55,7 +55,7 @@ def temp_hdf5_file(): @pytest.fixture(scope="class") def temp_videos_dir(): """Create temporary MP4 files.""" - temp_dir = tempfile.mkdtemp() + temp_dir = tempfile.mkdtemp() # noqa: SIM115 video_paths = [] for demo_id in range(2): @@ -83,7 +83,7 @@ def temp_videos_dir(): @pytest.fixture def temp_output_file(): """Create temporary output file.""" - temp_file = tempfile.NamedTemporaryFile(suffix=".h5", delete=False) + temp_file = tempfile.NamedTemporaryFile(suffix=".h5", delete=False) # noqa: SIM115 yield temp_file.name # Cleanup os.remove(temp_file.name) diff --git a/scripts/tools/test/test_train_and_publish_checkpoints.py b/scripts/tools/test/test_train_and_publish_checkpoints.py new file mode 100644 index 00000000..f36ab17f --- /dev/null +++ b/scripts/tools/test/test_train_and_publish_checkpoints.py @@ -0,0 +1,208 @@ +# Copyright (c) 2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Tests for the pretrained-checkpoint training utility.""" + +from argparse import Namespace +from pathlib import Path +from types import SimpleNamespace + +import gymnasium as gym +import pytest + +from isaaclab_tasks.utils.hydra import collect_presets +from isaaclab_tasks.utils.parse_cfg import load_cfg_from_registry +from isaaclab_tasks.utils.preset_cli import enumerate_task_presets +from isaaclab_tasks.utils.preset_target import PresetTarget + +from scripts.tools.train_and_publish_checkpoints import ( + CheckpointJob, + _build_core_jobs, + _play_command, + _select_physics_variants, + _training_command, + collect_pretrained_checkpoint, + publish_pretrained_checkpoint, +) + + +def test_cartpole_feature_presets_are_in_pretrained_checkpoint_matrix() -> None: + """Every Cartpole feature policy for the preferred workflow must receive a distinct checkpoint.""" + task_spec = gym.spec("Isaac-Cartpole-Camera") + workflow = task_spec.kwargs["default_agent"] + agent_cfg = load_cfg_from_registry(task_spec.id, f"{workflow}_cfg_entry_point") + feature_presets = set(collect_presets(agent_cfg)[""]) - {"default"} + + assert set(task_spec.kwargs["pretrained_checkpoint_preset_compatibility"][workflow]) == feature_presets + + +def test_checkpoint_preset_metadata_references_registered_variants() -> None: + """Checkpoint declarations must name registered workflows and domain presets.""" + for task_spec in gym.registry.values(): + checkpoint_compatibility = task_spec.kwargs.get("pretrained_checkpoint_preset_compatibility", {}) + if not checkpoint_compatibility: + continue + + preset_map = enumerate_task_presets(task_spec.id) or {} + domain_presets = set(preset_map.get(PresetTarget.DOMAIN, ())) + for workflow, preset_names in checkpoint_compatibility.items(): + assert f"{workflow}_cfg_entry_point" in task_spec.kwargs, ( + f"{task_spec.id}: unregistered {workflow} workflow" + ) + assert len(preset_names) == len(set(preset_names)), ( + f"{task_spec.id}: duplicate {workflow} checkpoint preset" + ) + assert not set(preset_names) - domain_presets, f"{task_spec.id}: unknown {workflow} checkpoint preset" + + +def test_build_core_jobs_skips_unsupported_preset_without_normalizing_default( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """An unsupported preset-only task must not abort construction of the supported core matrix.""" + task_spec = SimpleNamespace( + id="Isaac-Unsupported-Core-Task", + kwargs={ + "env_cfg_entry_point": "isaaclab_tasks.core.unsupported:UnsupportedEnvCfg", + "rsl_rl_cfg_entry_point": "isaaclab_tasks.core.unsupported:UnsupportedAgentCfg", + }, + ) + monkeypatch.setattr("scripts.tools.train_and_publish_checkpoints.gym.registry", {task_spec.id: task_spec}) + monkeypatch.setattr("scripts.tools.train_and_publish_checkpoints.parse_env_cfg", lambda _: object()) + monkeypatch.setattr( + "scripts.tools.train_and_publish_checkpoints.enumerate_task_presets", + lambda _: {PresetTarget.PHYSICS: ["newton_kamino"]}, + ) + monkeypatch.setattr( + "scripts.tools.train_and_publish_checkpoints.get_pretrained_checkpoint_backend_names", + lambda _: pytest.fail("preset-only tasks must not normalize their unsupported default backend"), + ) + args = Namespace(physics_backends="physx,newtonmjwarp", render_backends="rtx,newton") + + assert _build_core_jobs(args) == [] + + +def test_job_commands_use_uv_run_isaaclab() -> None: + """Training and playback must use the uv-managed Isaac Lab CLI.""" + job = CheckpointJob( + workflow="rsl_rl", + task_name="Isaac-Test", + physics_backend="physx", + render_backend="none", + preset_names=("depth",), + physics_selector="isaacsim_physx", + ) + args = Namespace(max_iterations=None, num_envs=None) + + train_command = _training_command(job, args, smoke=False) + play_command = _play_command(job, args, "/tmp/checkpoint.pt") + + assert train_command[:4] == ["uv", "run", "isaaclab", "train"] + assert play_command[:4] == ["uv", "run", "isaaclab", "play"] + assert train_command[-2:] == ["physics=isaacsim_physx", "presets=depth"] + assert play_command[-2:] == ["physics=isaacsim_physx", "presets=depth"] + + +def test_build_core_jobs_includes_declared_checkpoint_presets(monkeypatch: pytest.MonkeyPatch) -> None: + """Core jobs must include preset-specific checkpoints declared by the task.""" + task_spec = SimpleNamespace( + id="Isaac-Test", + kwargs={ + "env_cfg_entry_point": "isaaclab_tasks.core.test:TestEnvCfg", + "rl_games_cfg_entry_point": "isaaclab_tasks.core.test:TestAgentCfg", + "rsl_rl_cfg_entry_point": "isaaclab_tasks.core.test:TestAgentCfg", + "pretrained_checkpoint_preset_compatibility": {"rl_games": ("depth",)}, + }, + ) + monkeypatch.setattr("scripts.tools.train_and_publish_checkpoints.gym.registry", {task_spec.id: task_spec}) + monkeypatch.setattr("scripts.tools.train_and_publish_checkpoints.parse_env_cfg", lambda _: object()) + monkeypatch.setattr("scripts.tools.train_and_publish_checkpoints.enumerate_task_presets", lambda _: {}) + monkeypatch.setattr( + "scripts.tools.train_and_publish_checkpoints.get_pretrained_checkpoint_backend_names", + lambda _: ("physx", "rtx"), + ) + args = Namespace(physics_backends="physx", render_backends="rtx") + + jobs = _build_core_jobs(args) + + assert [(job.workflow, job.preset_names) for job in jobs] == [("rsl_rl", ()), ("rl_games", ("depth",))] + + +def test_select_physics_variants_uses_concrete_isaac_sim_physx() -> None: + """The normalized PhysX job must not resolve through the automatic selector.""" + variants = ["physx", "isaacsim_physx", "ovphysx", "newton_mjwarp"] + + selections = _select_physics_variants("Isaac-Test", variants, "physx", ["physx", "newtonmjwarp"]) + + assert selections == [("physx", "isaacsim_physx"), ("newtonmjwarp", "newton_mjwarp")] + + +def test_select_physics_variants_includes_franka_osc_newton_mjwarp() -> None: + """The effort-limited OSC task is supported by Newton MJWarp.""" + selections = _select_physics_variants( + "Isaac-Reach-Franka-OSC", ["isaacsim_physx", "newton_mjwarp"], "physx", ["newtonmjwarp"] + ) + + assert selections == [("newtonmjwarp", "newton_mjwarp")] + + +def test_select_physics_variants_selects_coupled_newton_preset() -> None: + """Coupled tasks must publish under the MJWarp backend using their proxy preset.""" + variants = ["physx", "isaacsim_physx", "ovphysx", "newton_mjwarp_vbd_proxy"] + + selections = _select_physics_variants("Isaac-Test", variants, "newtonmjwarp", ["newtonmjwarp"]) + + assert selections == [("newtonmjwarp", "newton_mjwarp_vbd_proxy")] + + +def test_select_physics_variants_does_not_fall_back_to_automatic_physx() -> None: + """A task without a concrete Isaac Sim selector must not run as OvPhysX.""" + selections = _select_physics_variants("Isaac-Test", ["physx", "ovphysx"], "physx", ["physx"]) + + assert selections == [] + + +def test_legacy_job_experiment_name_preserves_task_name() -> None: + """Legacy jobs must keep separate experiment directories for each task.""" + job = CheckpointJob(workflow="rsl_rl", task_name="Isaac-Test") + + assert job.experiment_name == "Isaac-Test" + + +def test_legacy_collection_preserves_task_directory(tmp_path: Path) -> None: + """Legacy collected checkpoints must retain their task-specific directory.""" + job = CheckpointJob(workflow="rsl_rl", task_name="Isaac-Test") + + path = collect_pretrained_checkpoint(job, str(tmp_path), dry_run=True) + + assert path == str(tmp_path / "rsl_rl" / "Isaac-Test" / "checkpoint.pt") + + +def test_publish_uses_collected_checkpoint_without_training_logs( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """Publishing a collected checkpoint must not require its original training logs.""" + job = CheckpointJob( + workflow="rsl_rl", + task_name="Isaac-Test", + physics_backend="newtonmjwarp", + render_backend="none", + preset_names=("depth",), + ) + collected_path = tmp_path / "rsl_rl" / "Isaac-Test_depth_newtonmjwarp_none_rsl_rl.pt" + collected_path.parent.mkdir() + collected_path.touch() + args = Namespace( + dry_run=True, + force_publish=True, + output_dir=str(tmp_path), + publish_root="omniverse://checkpoints", + ) + + assert publish_pretrained_checkpoint(job, args) + assert ( + f"Publishing {collected_path} -> omniverse://checkpoints/rsl_rl/Isaac-Test_depth_newtonmjwarp_none_rsl_rl.pt" + in capsys.readouterr().out + ) diff --git a/scripts/tools/test/test_uw_entrypoints.py b/scripts/tools/test/test_uw_entrypoints.py new file mode 100644 index 00000000..84999a9d --- /dev/null +++ b/scripts/tools/test/test_uw_entrypoints.py @@ -0,0 +1,137 @@ +# Copyright (c) 2026, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import argparse +import ast +import runpy +import sys +from pathlib import Path +from types import ModuleType, SimpleNamespace + +import pytest + +ROOT = Path(__file__).resolve().parents[3] + + +@pytest.mark.parametrize( + "keyword, expected", + [(None, ["Isaac-Test", "UW-Test", "OmniReset-Test"]), ("UW-", ["UW-Test"]), ("OmniReset-", ["OmniReset-Test"])], +) +def test_environment_listing_includes_uw_tasks(monkeypatch, capsys, keyword, expected): + gym = ModuleType("gymnasium") + gym.registry = { + name: SimpleNamespace( + id=name, + entry_point="fixture:Env", + kwargs={"env_cfg_entry_point": "fixture:Cfg", "deprecated": name == "Isaac-Old"}, + ) + for name in ("Isaac-Test", "UW-Test", "OmniReset-Test", "Isaac-Old", "Other-Test") + } + monkeypatch.setitem(sys.modules, "gymnasium", gym) + for name in ("isaaclab_tasks", "isaaclab_tasks_experimental", "uwlab_tasks"): + monkeypatch.setitem(sys.modules, name, ModuleType(name)) + argv = ["list_envs.py"] + (["--keyword", keyword] if keyword else []) + monkeypatch.setattr(sys, "argv", argv) + runpy.run_path(str(ROOT / "scripts/environments/list_envs.py"), run_name="__main__") + output = capsys.readouterr().out + for name in gym.registry: + assert (name in output) == (name in expected) + + +@pytest.mark.parametrize( + "arguments, resume", + [([], True), (["--experiment_name", "custom"], False), (["--resume"], False), (["--device", "cpu"], False)], +) +def test_rsl_cli_overrides_only_requested_values(arguments, resume): + module = runpy.run_path(str(ROOT / "scripts/reinforcement_learning/rsl_rl/cli_args.py")) + parser = argparse.ArgumentParser() + module["add_rsl_rl_args"](parser) + parser.add_argument("--device", default=None) + args = parser.parse_args(arguments) + cfg = SimpleNamespace( + seed=42, + resume=resume, + load_run="saved", + load_checkpoint="model.pt", + experiment_name="original", + run_name="base", + logger="tensorboard", + device="cuda:0", + ) + result = module["update_rsl_rl_cfg"](cfg, args) + assert result is cfg + assert cfg.resume is (resume or "--resume" in arguments) + assert cfg.experiment_name == ("custom" if "--experiment_name" in arguments else "original") + assert cfg.device == ("cpu" if "--device" in arguments else "cuda:0") + assert cfg.load_run == "saved" and cfg.load_checkpoint == "model.pt" + + +def _camera_configs(): + def load(path, names, namespace): + tree = ast.parse((ROOT / path).read_text()) + nodes = [node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names] + assert len(nodes) == len(names) + exec(compile(ast.Module(body=nodes, type_ignores=[]), path, "exec"), namespace) + return namespace + + class Sample: + def __init__(self, value): + self.value = value + + def sample(self): + return self.value + + tune = SimpleNamespace( + choice=lambda values: Sample(values[0]), + randint=lambda low, high: Sample(low), + sample_from=lambda function: Sample(function), + ) + base = load("scripts/reinforcement_learning/ray/tuner.py", ["JobCfg"], {}) + util = load("scripts/reinforcement_learning/ray/util.py", ["populate_isaac_ray_cfg_args"], {}) + vision = load( + "scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cfg.py", + ["CameraJobCfg", "ResNetCameraJob", "TheiaCameraJob"], + { + "tune": tune, + "tuner": SimpleNamespace(JobCfg=base["JobCfg"]), + "util": SimpleNamespace(populate_isaac_ray_cfg_args=util["populate_isaac_ray_cfg_args"]), + }, + ) + jobs = load( + "scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cartpole_cfg.py", + [ + "CartpoleRGBNoTuneJobCfg", + "CartpoleRGBCNNOnlyJobCfg", + "CartpoleRGBJobCfg", + "CartpoleResNetJobCfg", + "CartpoleTheiaJobCfg", + ], + {"tune": tune, "util": vision["util"], "vision_cfg": SimpleNamespace(**vision)}, + ) + return vision, jobs + + +@pytest.mark.parametrize( + "name", + [ + "CartpoleRGBNoTuneJobCfg", + "CartpoleRGBCNNOnlyJobCfg", + "CartpoleRGBJobCfg", + "CartpoleResNetJobCfg", + "CartpoleTheiaJobCfg", + ], +) +def test_camera_tuning_selects_matching_agent(name): + _, jobs = _camera_configs() + cfg = jobs[name]({}).cfg + assert cfg["runner_args"]["--rl_library"] == "rl_games" + assert cfg["hydra_args"]["agent.params.config.max_epochs"] == 200 + assert all(not key.startswith("agent.") or key.startswith("agent.params.") for key in cfg["hydra_args"]) + + +def test_camera_tuning_rejects_conflicting_agent(): + vision, _ = _camera_configs() + with pytest.raises(ValueError, match="rl_games"): + vision["CameraJobCfg"]({"runner_args": {"--task": "Isaac-Cartpole-Camera", "--rl_library": "rsl_rl"}}) diff --git a/scripts/tools/train_and_publish_checkpoints.py b/scripts/tools/train_and_publish_checkpoints.py new file mode 100644 index 00000000..e9b1cf69 --- /dev/null +++ b/scripts/tools/train_and_publish_checkpoints.py @@ -0,0 +1,811 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Train, collect, review, and publish pretrained Isaac Lab checkpoints. + +The core-task workflow selects RSL-RL when it is registered, falls back to +RL-Games, and uses SKRL MAPPO for multi-agent environments. Backend-aware +checkpoints are collected into one subdirectory per RL library: + +.. code-block:: text + + logs/pretrained_checkpoints/ + ├── rl_games/ + ├── rsl_rl/ + └── skrl/ + +Each checkpoint is named +``[_]___``. +State-only tasks use ``none`` as the render backend because their policies do +not depend on rendering. This workflow targets core tasks only; other +registered tasks do not receive published checkpoints from this matrix. The +core matrix excludes Newton Kamino presets. + +Examples: + +.. code-block:: shell + + # List the preferred core-task training matrix. + uv run python scripts/tools/train_and_publish_checkpoints.py \ + --list --all --core + + # Smoke-test every supported core-task backend combination. + uv run python scripts/tools/train_and_publish_checkpoints.py \ + --smoke --all --core + + # Run full training and collect checkpoints. + uv run python scripts/tools/train_and_publish_checkpoints.py \ + --train --all --core + + # Resume collection after training jobs have completed. + uv run python scripts/tools/train_and_publish_checkpoints.py \ + --collect --all --core + + # Select legacy jobs with workflow and task wildcards. + rl_games:Isaac-Humanoid-* # Wildcard for any Humanoid version + rsl_rl:Isaac-Ant-* # Wildcard for any Ant environment + *:IsaacContrib-Velocity-Flat-Spot # Wildcard for any workflow, specific task + +Legacy ``workflow:task`` job selectors remain supported when ``--core`` is +omitted. +""" + +from __future__ import annotations + +import argparse +import csv +import fnmatch +import json +import os +import posixpath +import shutil +import subprocess +import sys +from dataclasses import dataclass + +# Prefer this checkout over editable installs that may point at another worktree. +_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +_SOURCE_ROOT = os.path.join(_REPO_ROOT, "source") +_REPO_SOURCE_PATHS = sorted( + entry.path for entry in os.scandir(_SOURCE_ROOT) if entry.is_dir() and os.path.isdir(entry.path) +) +for source_path in reversed(_REPO_SOURCE_PATHS): + if source_path not in sys.path: + sys.path.insert(0, source_path) + +import gymnasium as gym + +from isaaclab.envs import DirectMARLEnvCfg + +from isaaclab_rl.utils.pretrained_checkpoint import ( + WORKFLOW_EXPERIMENT_NAME_VARIABLE, + WORKFLOWS, + get_latest_job_run_path, + get_pretrained_checkpoint_backend_names, + get_pretrained_checkpoint_filename, + get_pretrained_checkpoint_path, + get_pretrained_checkpoint_publish_path, + get_pretrained_checkpoint_review, + get_pretrained_checkpoint_review_path, + has_pretrained_checkpoint_job_finished, + has_pretrained_checkpoint_job_run, + has_pretrained_checkpoints_asset_root_dir, +) + +import isaaclab_tasks # noqa: F401 +from isaaclab_tasks.utils import parse_env_cfg +from isaaclab_tasks.utils.hydra import resolve_task_config +from isaaclab_tasks.utils.preset_cli import enumerate_task_presets +from isaaclab_tasks.utils.preset_target import PresetTarget + +_TRAINING_COMPLETE_FILENAME = ".pretrained_checkpoint_training_complete" +_CORE_WORKFLOWS = ("rl_games", "rsl_rl", "skrl") + + +@dataclass(frozen=True) +class CheckpointJob: + """One workflow, task, preset, physics, and renderer training combination.""" + + workflow: str + task_name: str + physics_backend: str | None = None + render_backend: str | None = None + preset_names: tuple[str, ...] = () + physics_selector: str | None = None + render_selector: str | None = None + agent: str | None = None + algorithm: str | None = None + + @property + def job_id(self) -> str: + """Return the stable human-readable job identifier.""" + if self.physics_backend is None: + return f"{self.workflow}:{self.task_name}" + parts = [self.workflow, self.task_name, *self.preset_names, self.physics_backend, self.render_backend] + return ":".join(parts) + + @property + def experiment_name(self) -> str: + """Return the experiment directory name used by the RL workflow.""" + if self.physics_backend is None and self.render_backend is None: + return self.task_name + filename = get_pretrained_checkpoint_filename( + self.workflow, + self.task_name, + self.physics_backend, + self.render_backend, + preset_names=self.preset_names, + ) + extension = os.path.splitext(filename)[1] + return filename.removesuffix(extension) + + @property + def preset_args(self) -> list[str]: + """Return preset selectors for this job.""" + args = [] + if self.physics_selector is not None: + args.append(f"physics={self.physics_selector}") + if self.render_selector is not None: + args.append(f"renderer={self.render_selector}") + if self.preset_names: + args.append(f"presets={','.join(self.preset_names)}") + return args + + +def _create_parser() -> argparse.ArgumentParser: + """Create the command-line parser.""" + parser = argparse.ArgumentParser( + description="Train and manage backend-aware pretrained checkpoints.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "jobs", + nargs="*", + help="Job patterns. Legacy jobs use workflow:task. Core matrix patterns match the displayed job IDs.", + ) + parser.add_argument("-t", "--train", action="store_true", help="Run full training and collect checkpoints.") + parser.add_argument("--smoke", action="store_true", help="Run one-iteration backend smoke training.") + parser.add_argument("--collect", action="store_true", help="Collect completed checkpoints without training.") + parser.add_argument("-p", "--publish_checkpoint", action="store_true", help="Publish collected checkpoints.") + parser.add_argument("-r", "--review", action="store_true", help="Interactively review checkpoints.") + parser.add_argument("-l", "--list", action="store_true", help="List matching checkpoint jobs.") + parser.add_argument("-f", "--force", action="store_true", help="Repeat completed training jobs.") + parser.add_argument("-a", "--all", action="store_true", help="Match all jobs in the selected scope.") + parser.add_argument( + "--core", + action="store_true", + help="Use core tasks, preferred RL libraries, and the supported backend matrix.", + ) + parser.add_argument( + "--physics_backends", + default="physx,newtonmjwarp", + help="Comma-separated normalized physics backends for --core (default: physx,newtonmjwarp).", + ) + parser.add_argument( + "--render_backends", + default="rtx,newton", + help="Comma-separated normalized camera render backends for --core (default: rtx,newton).", + ) + parser.add_argument( + "--output_dir", + default=os.path.join("logs", "pretrained_checkpoints"), + help="Structured checkpoint output directory.", + ) + parser.add_argument( + "--publish_root", + default=None, + help="Writable Nucleus root ending in PretrainedCheckpoints; defaults to the configured asset root.", + ) + parser.add_argument( + "-E", + "--exclude", + action="append", + default=[], + help="Exclude jobs matching a wildcard pattern.", + ) + parser.add_argument("--num_envs", type=int, default=None, help="Override the training environment count.") + parser.add_argument( + "--max_iterations", + type=int, + default=None, + help="Override full-training iterations. Omit to use each agent config's full schedule.", + ) + parser.add_argument("--force_review", action="store_true", help="Replace an existing review.") + parser.add_argument("--force_publish", action="store_true", help="Publish without an accepted review.") + parser.add_argument("--fail_fast", action="store_true", help="Stop after the first failed job.") + parser.add_argument("--dry_run", action="store_true", help="Print commands without running them.") + return parser + + +def _parse_backend_list(value: str, supported: set[str], option: str) -> list[str]: + """Parse and validate a comma-separated backend option.""" + backends = [item.strip() for item in value.split(",") if item.strip()] + unknown = set(backends) - supported + if unknown: + raise ValueError(f"{option} contains unsupported values: {sorted(unknown)}") + return backends + + +def _is_core_task(task_spec: gym.EnvSpec) -> bool: + """Return whether a Gym task is registered from ``isaaclab_tasks.core``.""" + env_cfg_entry_point = task_spec.kwargs.get("env_cfg_entry_point") + return ( + isinstance(env_cfg_entry_point, str) + and env_cfg_entry_point.startswith("isaaclab_tasks.core.") + and not task_spec.kwargs.get("deprecated") + ) + + +def _select_workflow(task_spec: gym.EnvSpec, env_cfg) -> tuple[str, str | None, str | None]: + """Select the preferred RL workflow and optional agent arguments.""" + if isinstance(env_cfg, DirectMARLEnvCfg): + if "skrl_mappo_cfg_entry_point" not in task_spec.kwargs: + raise ValueError(f"Multi-agent task {task_spec.id!r} does not register an SKRL MAPPO config") + return "skrl", "skrl_mappo_cfg_entry_point", "MAPPO" + if "rsl_rl_cfg_entry_point" in task_spec.kwargs: + return "rsl_rl", None, None + if "rl_games_cfg_entry_point" in task_spec.kwargs: + return "rl_games", None, None + raise ValueError(f"Task {task_spec.id!r} has neither an RSL-RL nor an RL-Games config") + + +def _select_physics_variants( + task_name: str, + variants: list[str], + default_backend: str | None, + requested_backends: list[str], +) -> list[tuple[str, str | None]]: + """Return normalized physics backends and their task preset selectors.""" + selections = [] + for backend in requested_backends: + selector = None + if variants: + if backend == "physx" and "isaacsim_physx" in variants: + selector = "isaacsim_physx" + elif backend == "newtonmjwarp": + selector = next( + ( + candidate + for candidate in ("newton_mjwarp", "newton_mjwarp_vbd", "newton_mjwarp_vbd_proxy") + if candidate in variants + ), + None, + ) + if selector is None: + continue + elif backend != default_backend: + continue + selections.append((backend, selector)) + return selections + + +def _resolve_physics_backend(task_name: str, physics_selector: str | None, default_backend: str | None) -> str: + """Return the checkpoint physics token produced by a task's selected physics preset. + + The token names the solver tree, so a preset selector and its published filename can + only be kept in agreement by resolving the selector. + """ + if physics_selector is None: + return default_backend + env_cfg, _ = resolve_task_config(task_name, None, overrides=(f"physics={physics_selector}",)) + physics_backend, _ = get_pretrained_checkpoint_backend_names(env_cfg) + return physics_backend + + +def _select_render_variants( + variants: list[str], + requested_backends: list[str], +) -> list[tuple[str, str | None]]: + """Return normalized render backends and their task preset selectors.""" + if not variants: + return [("none", None)] + + selections = [] + for backend in requested_backends: + selector = None + if backend == "rtx": + selector = next( + (candidate for candidate in ("isaacsim_rtx", "rtx", "ovrtx") if candidate in variants), None + ) + elif backend == "newton" and "newton_renderer" in variants: + selector = "newton_renderer" + if selector is not None: + selections.append((backend, selector)) + return selections + + +def _build_core_jobs(args: argparse.Namespace) -> list[CheckpointJob]: + """Build the supported preferred-workflow matrix for core tasks.""" + physics_backends = _parse_backend_list( + args.physics_backends, + {"newtonmjwarp", "physx"}, + "--physics_backends", + ) + render_backends = _parse_backend_list( + args.render_backends, + {"newton", "rtx"}, + "--render_backends", + ) + jobs = [] + for task_spec in sorted(gym.registry.values(), key=lambda spec: spec.id): + if not _is_core_task(task_spec): + continue + + preset_map = enumerate_task_presets(task_spec.id) or {} + physics_variants = preset_map.get(PresetTarget.PHYSICS, []) + render_variants = preset_map.get(PresetTarget.RENDERER, []) + env_cfg = parse_env_cfg(task_spec.id) + preferred_workflow = _select_workflow(task_spec, env_cfg) + checkpoint_compatibility = task_spec.kwargs.get("pretrained_checkpoint_preset_compatibility", {}) + workflow_selections = [preferred_workflow] + workflow_selections.extend( + (workflow, None, None) + for workflow in checkpoint_compatibility + if workflow != preferred_workflow[0] and f"{workflow}_cfg_entry_point" in task_spec.kwargs + ) + default_physics = None + if not physics_variants: + default_physics, _ = get_pretrained_checkpoint_backend_names(env_cfg) + + physics_selections = _select_physics_variants( + task_spec.id, + physics_variants, + default_physics, + physics_backends, + ) + render_selections = _select_render_variants(render_variants, render_backends) + for workflow, agent, algorithm in workflow_selections: + checkpoint_presets = [()] if workflow == preferred_workflow[0] else [] + checkpoint_presets.extend((preset_name,) for preset_name in checkpoint_compatibility.get(workflow, ())) + for _physics_family, physics_selector in physics_selections: + physics_backend = _resolve_physics_backend(task_spec.id, physics_selector, default_physics) + for render_backend, render_selector in render_selections: + for preset_names in checkpoint_presets: + jobs.append( + CheckpointJob( + workflow=workflow, + task_name=task_spec.id, + physics_backend=physics_backend, + render_backend=render_backend, + preset_names=preset_names, + physics_selector=physics_selector, + render_selector=render_selector, + agent=agent, + algorithm=algorithm, + ) + ) + return jobs + + +def _build_legacy_jobs() -> list[CheckpointJob]: + """Build every legacy workflow/task pair registered with Gym.""" + jobs = [] + for workflow in WORKFLOWS: + for task_spec in sorted(gym.registry.values(), key=lambda spec: spec.id): + if workflow + "_cfg_entry_point" in task_spec.kwargs and not task_spec.kwargs.get("deprecated"): + jobs.append(CheckpointJob(workflow=workflow, task_name=task_spec.id)) + return jobs + + +def _filter_jobs(jobs: list[CheckpointJob], args: argparse.Namespace) -> list[CheckpointJob]: + """Apply positional include patterns and exclusion patterns.""" + include_patterns = ["*"] if args.all else args.jobs + selected = [] + for job in jobs: + if not any(fnmatch.fnmatch(job.job_id, pattern) for pattern in include_patterns): + continue + if any(fnmatch.fnmatch(job.job_id, pattern) for pattern in args.exclude): + continue + selected.append(job) + return selected + + +def _training_command(job: CheckpointJob, args: argparse.Namespace, smoke: bool) -> list[str]: + """Build the unified training command for a checkpoint job.""" + command = [ + "uv", + "run", + "isaaclab", + "train", + "--rl_library", + job.workflow, + "--task", + job.task_name, + ] + if job.agent is not None: + command.extend(["--agent", job.agent]) + if job.algorithm is not None: + command.extend(["--algorithm", job.algorithm]) + + experiment_name = f"{job.experiment_name}_smoke" if smoke else job.experiment_name + experiment_variable = WORKFLOW_EXPERIMENT_NAME_VARIABLE[job.workflow] + if experiment_variable is not None: + command.append(f"{experiment_variable}={experiment_name}") + + if smoke: + command.extend(["--max_iterations", "1", "--num_envs", str(args.num_envs or 4)]) + else: + if args.max_iterations is not None: + command.extend(["--max_iterations", str(args.max_iterations)]) + if args.num_envs is not None: + command.extend(["--num_envs", str(args.num_envs)]) + command.extend(job.preset_args) + return command + + +def _play_command(job: CheckpointJob, args: argparse.Namespace, checkpoint_path: str) -> list[str]: + """Build the unified playback command for a checkpoint job.""" + command = [ + "uv", + "run", + "isaaclab", + "play", + "--rl_library", + job.workflow, + "--task", + job.task_name, + "--checkpoint", + checkpoint_path, + ] + if job.agent is not None: + command.extend(["--agent", job.agent]) + if job.algorithm is not None: + command.extend(["--algorithm", job.algorithm]) + if args.num_envs is not None: + command.extend(["--num_envs", str(args.num_envs)]) + command.extend(job.preset_args) + return command + + +def _run_command(command: list[str], dry_run: bool) -> int: + """Print and run a subprocess command.""" + print("Running:", " ".join(command), flush=True) + if dry_run: + return 0 + env = os.environ.copy() + existing_pythonpath = env.get("PYTHONPATH") + python_paths = _REPO_SOURCE_PATHS + ([existing_pythonpath] if existing_pythonpath else []) + env["PYTHONPATH"] = os.pathsep.join(python_paths) + env.pop("CONDA_PREFIX", None) + # Keep the active environment ahead of Isaac Sim's bundled site-packages. + # This matters for packages such as Newton, where the project environment + # can carry the source-compatible revision while Isaac Sim bundles an older + # release. + env["VIRTUAL_ENV"] = sys.prefix + return subprocess.run(command, check=False, cwd=_REPO_ROOT, env=env).returncode + + +def _has_training_job_completed(job: CheckpointJob) -> bool: + """Return whether the latest run exited successfully with a checkpoint.""" + run_path = get_latest_job_run_path( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + if run_path is None or not os.path.isfile(os.path.join(run_path, _TRAINING_COMPLETE_FILENAME)): + return False + return has_pretrained_checkpoint_job_finished( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + + +def _mark_training_job_completed(job: CheckpointJob) -> None: + """Record that the latest training subprocess exited successfully.""" + run_path = get_latest_job_run_path( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + if run_path is None: + raise RuntimeError(f"Unable to determine the latest run for {job.job_id}") + marker_path = os.path.join(run_path, _TRAINING_COMPLETE_FILENAME) + with open(marker_path, "w", encoding="utf-8") as marker_file: + marker_file.write(f"{job.job_id}\n") + + +def train_job(job: CheckpointJob, args: argparse.Namespace, smoke: bool = False) -> bool: + """Train or smoke-test one checkpoint job.""" + if not smoke and not args.force and _has_training_job_completed(job): + print(f"Skipping completed training job {job.job_id}") + return True + + result = _run_command(_training_command(job, args, smoke), args.dry_run) + if result != 0: + print(f"Training failed for {job.job_id} with exit code {result}", file=sys.stderr) + return False + if smoke or args.dry_run: + return True + if not has_pretrained_checkpoint_job_finished( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ): + print(f"Training did not produce a checkpoint for {job.job_id}", file=sys.stderr) + return False + _mark_training_job_completed(job) + return True + + +def collect_pretrained_checkpoint(job: CheckpointJob, output_dir: str, dry_run: bool = False) -> str | None: + """Copy the last or best checkpoint into the structured output directory.""" + destination = _get_collected_checkpoint_path(job, output_dir) + if dry_run: + print(f"Would collect the completed checkpoint -> {destination}") + return destination + + source_path = get_pretrained_checkpoint_path( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + if source_path is None or not os.path.isfile(source_path): + print(f"No completed checkpoint to collect for {job.job_id}") + return None + + print(f"Collecting {source_path} -> {destination}") + os.makedirs(os.path.dirname(destination), exist_ok=True) + shutil.copy2(source_path, destination) + return destination + + +def review_pretrained_checkpoint(job: CheckpointJob, args: argparse.Namespace) -> bool: + """Play and interactively review one checkpoint.""" + if not has_pretrained_checkpoint_job_run( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ): + print(f"Skipping review of {job.job_id}; it has not been trained") + return False + if not has_pretrained_checkpoint_job_finished( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ): + print(f"Skipping review of {job.job_id}; training is incomplete") + return False + + review = get_pretrained_checkpoint_review( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + if not args.force_review and review and review.get("reviewed"): + print(f"Review already complete for {job.job_id}") + return True + + checkpoint_path = get_pretrained_checkpoint_path( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + if checkpoint_path is None: + print(f"Skipping review of {job.job_id}; no checkpoint was found") + return False + command = _play_command(job, args, checkpoint_path) + if _run_command(command, args.dry_run) != 0: + return False + if args.dry_run: + return True + + answer = input(f"Accept checkpoint {job.job_id}? yes, no, or undetermined (y/n/u) [u]: ").strip().lower() + answer_map = {"y": "accepted", "n": "rejected", "u": "undetermined"} + result = answer_map.get(answer, "undetermined") + notes = input("Review notes (optional): ").strip() + review_data = {"reviewed": True, "result": result} + if notes: + review_data["notes"] = notes + + review_path = get_pretrained_checkpoint_review_path( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + if review_path is None: + raise RuntimeError(f"Unable to determine review path for {job.job_id}") + with open(review_path, "w", encoding="utf-8") as review_file: + json.dump(review_data, review_file, indent=4) + return True + + +def publish_pretrained_checkpoint(job: CheckpointJob, args: argparse.Namespace) -> bool: + """Publish an accepted checkpoint to the configured Nucleus asset root.""" + if args.publish_root is None and not has_pretrained_checkpoints_asset_root_dir(): + raise RuntimeError("A pretrained-checkpoint Nucleus asset root is not configured") + local_path = _get_collected_checkpoint_path(job, args.output_dir) + if not os.path.isfile(local_path): + print(f"Skipping publish of {job.job_id}; no collected checkpoint was found") + return False + + if not args.force_publish: + review = get_pretrained_checkpoint_review( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + if not review or review.get("result") != "accepted": + print(f"Skipping publish of {job.job_id}; it does not have an accepted review") + return False + + if args.publish_root is None: + publish_path = get_pretrained_checkpoint_publish_path( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + else: + filename = get_pretrained_checkpoint_filename( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + publish_path = posixpath.join(args.publish_root.rstrip("/"), job.workflow, filename) + print(f"Publishing {local_path} -> {publish_path}") + if args.dry_run: + return True + + import omni.client + from omni.client._omniclient import CopyBehavior + + result = omni.client.copy_file(local_path, publish_path, CopyBehavior.OVERWRITE) + if result != omni.client.Result.OK: + print(f"Publishing failed for {job.job_id}: {result}", file=sys.stderr) + return False + return True + + +def _summary_row(job: CheckpointJob, output_dir: str) -> list[str | bool]: + """Return one CSV summary row.""" + has_run = has_pretrained_checkpoint_job_run( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + has_finished = ( + _has_training_job_completed(job) + if job.physics_backend is not None + else has_pretrained_checkpoint_job_finished(job.workflow, job.task_name) + ) + collected_path = _get_collected_checkpoint_path(job, output_dir) + review = get_pretrained_checkpoint_review( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + return [ + job.workflow, + job.task_name, + ",".join(job.preset_names), + job.physics_backend or "", + job.render_backend or "", + job.physics_selector or "", + job.render_selector or "", + has_run, + has_finished, + os.path.isfile(collected_path), + (review or {}).get("result", ""), + ] + + +def _get_collected_checkpoint_path(job: CheckpointJob, output_dir: str) -> str: + """Return the absolute path of a checkpoint in the collection directory.""" + filename = get_pretrained_checkpoint_filename( + job.workflow, + job.task_name, + job.physics_backend, + job.render_backend, + preset_names=job.preset_names, + ) + path_parts = [output_dir, job.workflow] + if job.physics_backend is None: + path_parts.append(job.task_name) + return os.path.abspath(os.path.join(*path_parts, filename)) + + +def main(argv: list[str] | None = None) -> int: + """Run checkpoint management actions.""" + parser = _create_parser() + args = parser.parse_args(argv) + if not args.all and not args.jobs: + parser.error("provide one or more job patterns, or pass --all") + if not any((args.train, args.smoke, args.collect, args.publish_checkpoint, args.review, args.list)): + parser.error( + "select at least one action: --train, --smoke, --collect, --publish_checkpoint, --review, or --list" + ) + if args.train and args.smoke: + parser.error("--train and --smoke are separate actions") + + jobs = _build_core_jobs(args) if args.core else _build_legacy_jobs() + jobs = _filter_jobs(jobs, args) + if not jobs: + print("No jobs matched the requested scope and patterns.", file=sys.stderr) + return 1 + if args.core and (args.train or args.collect) and not args.dry_run: + for workflow in _CORE_WORKFLOWS: + os.makedirs(os.path.join(args.output_dir, workflow), exist_ok=True) + + if args.list: + writer = csv.writer(sys.stdout, quotechar='"', quoting=csv.QUOTE_MINIMAL) + writer.writerow( + [ + "Workflow", + "Task", + "Presets", + "Physics", + "Renderer", + "Physics selector", + "Renderer selector", + "Ran", + "Finished", + "Collected", + "Review", + ] + ) + writer.writerows(_summary_row(job, args.output_dir) for job in jobs) + return 0 + + failed_jobs = [] + for job in jobs: + success = True + if args.smoke: + success = train_job(job, args, smoke=True) + if args.train: + success = train_job(job, args) + if success: + success = collect_pretrained_checkpoint(job, args.output_dir, args.dry_run) is not None + elif args.collect: + success = collect_pretrained_checkpoint(job, args.output_dir, args.dry_run) is not None + if success and args.review: + success = review_pretrained_checkpoint(job, args) + if success and args.publish_checkpoint: + success = publish_pretrained_checkpoint(job, args) + + if not success: + failed_jobs.append(job.job_id) + if args.fail_fast: + break + + if failed_jobs: + print("Failed jobs:", file=sys.stderr) + for job_id in failed_jobs: + print(f" {job_id}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/tools/usd_output.py b/scripts/tools/usd_output.py new file mode 100644 index 00000000..5da7cf72 --- /dev/null +++ b/scripts/tools/usd_output.py @@ -0,0 +1,38 @@ +# Copyright (c) 2026, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import os +from pathlib import Path + +from pxr import Sdf + + +def write_usd_entry_layer(generated_path: str, output_path: str) -> str: + """Expose structured importer output without relocating its assets. + + Args: + generated_path: Root USD layer produced by the importer. + output_path: Requested USD entry-layer filename. + + Returns: + Absolute path to the requested entry layer. + """ + generated_path = os.path.abspath(generated_path) + output_path = os.path.abspath(output_path) + source = Sdf.Layer.FindOrOpen(generated_path) + if source is None: + raise ValueError(f"Unable to open generated USD layer: {generated_path}") + if generated_path == output_path: + return output_path + if os.path.exists(output_path) and os.path.samefile(generated_path, output_path): + raise ValueError("Requested output aliases the generated layer; choose a distinct filename.") + entry = Sdf.Layer.CreateAnonymous() + for key in source.pseudoRoot.ListInfoKeys(): + if key not in {"subLayers", "subLayerOffsets"}: + entry.pseudoRoot.SetInfo(key, source.pseudoRoot.GetInfo(key)) + entry.subLayerPaths = [Path(os.path.relpath(generated_path, os.path.dirname(output_path))).as_posix()] + if not entry.Export(output_path): + raise RuntimeError(f"Unable to write USD entry layer: {output_path}") + return output_path diff --git a/scripts/tutorials/00_sim/create_empty.py b/scripts/tutorials/00_sim/create_empty.py index e922acc1..9729145b 100644 --- a/scripts/tutorials/00_sim/create_empty.py +++ b/scripts/tutorials/00_sim/create_empty.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -8,7 +8,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/00_sim/create_empty.py + uv run python scripts/tutorials/00_sim/create_empty.py """ diff --git a/scripts/tutorials/00_sim/launch_app.py b/scripts/tutorials/00_sim/launch_app.py index ddea3c7c..fb4c203d 100644 --- a/scripts/tutorials/00_sim/launch_app.py +++ b/scripts/tutorials/00_sim/launch_app.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,7 +9,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/00_sim/launch_app.py + uv run python scripts/tutorials/00_sim/launch_app.py """ @@ -70,7 +70,7 @@ def main(): """Main function.""" # Initialize the simulation context - sim_cfg = sim_utils.SimulationCfg(dt=0.01, device=args_cli.device) + sim_cfg = sim_utils.SimulationCfg(device=args_cli.device, dt=0.01) sim = sim_utils.SimulationContext(sim_cfg) # Set main camera sim.set_camera_view([2.0, 0.0, 2.5], [-0.5, 0.0, 0.5]) diff --git a/scripts/tutorials/00_sim/log_time.py b/scripts/tutorials/00_sim/log_time.py index 4169c9fe..b8d2e480 100644 --- a/scripts/tutorials/00_sim/log_time.py +++ b/scripts/tutorials/00_sim/log_time.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -10,7 +10,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/00_sim/log_time.py + uv run python scripts/tutorials/00_sim/log_time.py """ @@ -45,7 +45,7 @@ def main(): 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 + # all Python execution is done through the uv-managed workspace # and the calling process' path will be /workspace/isaaclab log_dir_path = os.path.abspath(os.path.join(log_dir_path, "docker_tutorial")) if not os.path.isdir(log_dir_path): diff --git a/scripts/tutorials/00_sim/set_rendering_mode.py b/scripts/tutorials/00_sim/set_rendering_mode.py deleted file mode 100644 index 49125d91..00000000 --- a/scripts/tutorials/00_sim/set_rendering_mode.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -"""This script demonstrates how to spawn prims into the scene. - -.. code-block:: bash - - # Usage - ./isaaclab.sh -p scripts/tutorials/00_sim/set_rendering_mode.py - -""" - -"""Launch Isaac Sim Simulator first.""" - - -import argparse - -from isaaclab.app import AppLauncher - -# create argparser -parser = argparse.ArgumentParser( - description="Tutorial on viewing a warehouse scene with a given rendering mode preset." -) -# 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 isaaclab.sim as sim_utils -from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR - - -def main(): - """Main function.""" - - # rendering modes include performance, balanced, and quality - # note, the rendering_mode specified in the CLI argument (--rendering_mode) takes precedence over this Render Config setting - rendering_mode = "performance" - - # carb setting dictionary can include any rtx carb setting which will overwrite the native preset setting - carb_settings = {"rtx.reflections.enabled": True} - - # Initialize render config - render_cfg = sim_utils.RenderCfg( - rendering_mode=rendering_mode, - carb_settings=carb_settings, - ) - - # Initialize the simulation context with render coofig - sim_cfg = sim_utils.SimulationCfg(render=render_cfg) - sim = sim_utils.SimulationContext(sim_cfg) - - # Pose camera in the hospital lobby area - sim.set_camera_view([-11, -0.5, 2], [0, 0, 0.5]) - - # Load hospital scene - hospital_usd_path = f"{ISAAC_NUCLEUS_DIR}/Environments/Hospital/hospital.usd" - cfg = sim_utils.UsdFileCfg(usd_path=hospital_usd_path) - cfg.func("/Scene", cfg) - - # Play the simulator - sim.reset() - - # Now we are ready! - print("[INFO]: Setup complete...") - - # Run simulation and view scene - while simulation_app.is_running(): - sim.step() - - -if __name__ == "__main__": - # run the main function - main() - # close sim app - simulation_app.close() diff --git a/scripts/tutorials/00_sim/spawn_prims.py b/scripts/tutorials/00_sim/spawn_prims.py index 0e1f02c3..cfc0a87a 100644 --- a/scripts/tutorials/00_sim/spawn_prims.py +++ b/scripts/tutorials/00_sim/spawn_prims.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -8,7 +8,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/00_sim/spawn_prims.py + uv run python scripts/tutorials/00_sim/spawn_prims.py """ @@ -31,7 +31,8 @@ """Rest everything follows.""" -import isaacsim.core.utils.prims as prim_utils +from isaaclab_physx.sim.schemas import PhysxDeformableBodyPropertiesCfg +from isaaclab_physx.sim.spawners.materials import PhysxDeformableBodyMaterialCfg import isaaclab.sim as sim_utils from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR @@ -51,7 +52,7 @@ def design_scene(): cfg_light_distant.func("/World/lightDistant", cfg_light_distant, translation=(1, 0, 10)) # create a new xform prim for all objects to be spawned under - prim_utils.create_prim("/World/Objects", "Xform") + sim_utils.create_prim("/World/Objects", "Xform") # spawn a red cone cfg_cone = sim_utils.ConeCfg( radius=0.15, @@ -77,9 +78,9 @@ def design_scene(): # spawn a blue cuboid with deformable body cfg_cuboid_deformable = sim_utils.MeshCuboidCfg( size=(0.2, 0.5, 0.2), - deformable_props=sim_utils.DeformableBodyPropertiesCfg(), + deformable_props=PhysxDeformableBodyPropertiesCfg(), visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0)), - physics_material=sim_utils.DeformableBodyMaterialCfg(), + physics_material=PhysxDeformableBodyMaterialCfg(), ) cfg_cuboid_deformable.func("/World/Objects/CuboidDeformable", cfg_cuboid_deformable, translation=(0.15, 0.0, 2.0)) diff --git a/scripts/tutorials/01_assets/add_new_robot.py b/scripts/tutorials/01_assets/add_new_robot.py index 43d31154..ca0c477e 100644 --- a/scripts/tutorials/01_assets/add_new_robot.py +++ b/scripts/tutorials/01_assets/add_new_robot.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -59,22 +59,22 @@ actuators={ "front_joints": ImplicitActuatorCfg( joint_names_expr=["joint[1-2]"], - effort_limit_sim=100.0, - velocity_limit_sim=100.0, + joint_effort_limit=100.0, + joint_velocity_limit=100.0, stiffness=10000.0, damping=100.0, ), "joint3_act": ImplicitActuatorCfg( joint_names_expr=["joint3"], - effort_limit_sim=100.0, - velocity_limit_sim=100.0, + joint_effort_limit=100.0, + joint_velocity_limit=100.0, stiffness=10000.0, damping=100.0, ), "joint4_act": ImplicitActuatorCfg( joint_names_expr=["joint4"], - effort_limit_sim=100.0, - velocity_limit_sim=100.0, + joint_effort_limit=100.0, + joint_velocity_limit=100.0, stiffness=10000.0, damping=100.0, ), @@ -103,34 +103,43 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): sim_time = 0.0 count = 0 + # wheel-velocity templates allocated once on the simulation device; the joint + # target setters dispatch to GPU Warp kernels and reject CPU tensors. + straight_action = torch.tensor([[10.0, 10.0]], device=sim.device).repeat(scene.num_envs, 1) + turn_action = torch.tensor([[5.0, -5.0]], device=sim.device).repeat(scene.num_envs, 1) + while simulation_app.is_running(): # reset if count % 500 == 0: # reset counters count = 0 # reset the scene entities to their initial positions offset by the environment origins - root_jetbot_state = scene["Jetbot"].data.default_root_state.clone() - root_jetbot_state[:, :3] += scene.env_origins - root_dofbot_state = scene["Dofbot"].data.default_root_state.clone() - root_dofbot_state[:, :3] += scene.env_origins + root_jetbot_pose = scene["Jetbot"].data.default_root_pose.torch.clone() + root_jetbot_pose[:, :3] += scene.env_origins + root_dofbot_pose = scene["Dofbot"].data.default_root_pose.torch.clone() + root_dofbot_pose[:, :3] += scene.env_origins # copy the default root state to the sim for the jetbot's orientation and velocity - scene["Jetbot"].write_root_pose_to_sim(root_jetbot_state[:, :7]) - scene["Jetbot"].write_root_velocity_to_sim(root_jetbot_state[:, 7:]) - scene["Dofbot"].write_root_pose_to_sim(root_dofbot_state[:, :7]) - scene["Dofbot"].write_root_velocity_to_sim(root_dofbot_state[:, 7:]) + scene["Jetbot"].write_root_pose_to_sim_index(root_pose=root_jetbot_pose) + root_jetbot_vel = scene["Jetbot"].data.default_root_vel.torch.clone() + scene["Jetbot"].write_root_velocity_to_sim_index(root_velocity=root_jetbot_vel) + scene["Dofbot"].write_root_pose_to_sim_index(root_pose=root_dofbot_pose) + root_dofbot_vel = scene["Dofbot"].data.default_root_vel.torch.clone() + scene["Dofbot"].write_root_velocity_to_sim_index(root_velocity=root_dofbot_vel) # copy the default joint states to the sim joint_pos, joint_vel = ( - scene["Jetbot"].data.default_joint_pos.clone(), - scene["Jetbot"].data.default_joint_vel.clone(), + scene["Jetbot"].data.default_joint_pos.torch.clone(), + scene["Jetbot"].data.default_joint_vel.torch.clone(), ) - scene["Jetbot"].write_joint_state_to_sim(joint_pos, joint_vel) + scene["Jetbot"].write_joint_position_to_sim_index(position=joint_pos) + scene["Jetbot"].write_joint_velocity_to_sim_index(velocity=joint_vel) joint_pos, joint_vel = ( - scene["Dofbot"].data.default_joint_pos.clone(), - scene["Dofbot"].data.default_joint_vel.clone(), + scene["Dofbot"].data.default_joint_pos.torch.clone(), + scene["Dofbot"].data.default_joint_vel.torch.clone(), ) - scene["Dofbot"].write_joint_state_to_sim(joint_pos, joint_vel) + scene["Dofbot"].write_joint_position_to_sim_index(position=joint_pos) + scene["Dofbot"].write_joint_velocity_to_sim_index(velocity=joint_vel) # clear internal buffers scene.reset() print("[INFO]: Resetting Jetbot and Dofbot state...") @@ -138,17 +147,17 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): # drive around if count % 100 < 75: # Drive straight by setting equal wheel velocities - action = torch.Tensor([[10.0, 10.0]]) + action = straight_action else: # Turn by applying different velocities - action = torch.Tensor([[5.0, -5.0]]) + action = turn_action - scene["Jetbot"].set_joint_velocity_target(action) + scene["Jetbot"].set_joint_velocity_target_index(target=action) # wave - wave_action = scene["Dofbot"].data.default_joint_pos + wave_action = scene["Dofbot"].data.default_joint_pos.torch.clone() wave_action[:, 0:4] = 0.25 * np.sin(2 * np.pi * 0.5 * sim_time) - scene["Dofbot"].set_joint_position_target(wave_action) + scene["Dofbot"].set_joint_position_target_index(target=wave_action) scene.write_data_to_sim() sim.step() @@ -164,7 +173,7 @@ def main(): sim = sim_utils.SimulationContext(sim_cfg) sim.set_camera_view([3.5, 0.0, 3.2], [0.0, 0.0, 0.5]) # Design scene - scene_cfg = NewRobotsSceneCfg(args_cli.num_envs, env_spacing=2.0) + scene_cfg = NewRobotsSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0) scene = InteractiveScene(scene_cfg) # Play the simulator sim.reset() diff --git a/scripts/tutorials/01_assets/run_articulation.py b/scripts/tutorials/01_assets/run_articulation.py index 869ef033..5607d48d 100644 --- a/scripts/tutorials/01_assets/run_articulation.py +++ b/scripts/tutorials/01_assets/run_articulation.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -8,7 +8,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/01_assets/run_articulation.py + uv run python scripts/tutorials/01_assets/run_articulation.py """ @@ -34,8 +34,6 @@ import torch -import isaacsim.core.utils.prims as prim_utils - import isaaclab.sim as sim_utils from isaaclab.assets import Articulation from isaaclab.sim import SimulationContext @@ -59,9 +57,9 @@ def design_scene() -> tuple[dict, list[list[float]]]: # Each group will have a robot in it origins = [[0.0, 0.0, 0.0], [-1.0, 0.0, 0.0]] # Origin 1 - prim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0]) + sim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0]) # Origin 2 - prim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) + sim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) # Articulation cartpole_cfg = CARTPOLE_CFG.copy() @@ -92,22 +90,27 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, Articula # 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] += origins - robot.write_root_pose_to_sim(root_state[:, :7]) - robot.write_root_velocity_to_sim(root_state[:, 7:]) + root_pose = robot.data.default_root_pose.torch.clone() + root_pose[:, :3] += origins + robot.write_root_pose_to_sim_index(root_pose=root_pose) + root_vel = robot.data.default_root_vel.torch.clone() + robot.write_root_velocity_to_sim_index(root_velocity=root_vel) # set joint positions with some noise - joint_pos, joint_vel = robot.data.default_joint_pos.clone(), robot.data.default_joint_vel.clone() + joint_pos, joint_vel = ( + robot.data.default_joint_pos.torch.clone(), + robot.data.default_joint_vel.torch.clone(), + ) joint_pos += torch.rand_like(joint_pos) * 0.1 - robot.write_joint_state_to_sim(joint_pos, joint_vel) + robot.write_joint_position_to_sim_index(position=joint_pos) + robot.write_joint_velocity_to_sim_index(velocity=joint_vel) # clear internal buffers robot.reset() print("[INFO]: Resetting robot state...") # Apply random action # -- generate random joint efforts - efforts = torch.randn_like(robot.data.joint_pos) * 5.0 + efforts = torch.randn_like(robot.data.joint_pos.torch) * 5.0 # -- apply action to the robot - robot.set_joint_effort_target(efforts) + robot.actuators.target_command.set_effort_index(value=efforts) # -- write data to sim robot.write_data_to_sim() # Perform step diff --git a/scripts/tutorials/01_assets/run_deformable_object.py b/scripts/tutorials/01_assets/run_deformable_object.py index ca7b841d..4b1b6d4d 100644 --- a/scripts/tutorials/01_assets/run_deformable_object.py +++ b/scripts/tutorials/01_assets/run_deformable_object.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -8,43 +8,56 @@ .. code-block:: bash - # Usage - ./isaaclab.sh -p scripts/tutorials/01_assets/run_deformable_object.py + # Usage with default PhysX physics and default kit visualizer. + uv run --extra isaacsim --extra tetrahedralization python scripts/tutorials/01_assets/run_deformable_object.py -""" + # Usage with Newton VBD physics and default kit visualizer. + uv run --extra isaacsim --extra tetrahedralization python scripts/tutorials/01_assets/run_deformable_object.py \ + --backend newton_vbd + + # Usage with OvPhysX physics without a visualizer. + uv run --extra ovphysx --extra tetrahedralization python scripts/tutorials/01_assets/run_deformable_object.py \ + --backend ovphysx -"""Launch Isaac Sim Simulator first.""" +""" +"""Parse CLI first so we can decide whether to launch Isaac Sim Kit.""" import argparse +from typing import TYPE_CHECKING -from isaaclab.app import AppLauncher +from isaaclab.app import add_launcher_args, launch_simulation # add argparse arguments parser = argparse.ArgumentParser(description="Tutorial on interacting with a deformable object.") -# append AppLauncher cli args -AppLauncher.add_app_launcher_args(parser) +parser.add_argument( + "--backend", type=str, default="physx", choices=["physx", "newton_vbd", "ovphysx"], help="Physics backend." +) +# append simulation launcher CLI arguments +add_launcher_args(parser) +# Kit cannot be combined with OvPhysX, so use no visualizer by default for that backend +backend_args, _ = parser.parse_known_args() +parser.set_defaults(visualizer=None if backend_args.backend == "ovphysx" else ["kit"]) # parse the arguments args_cli = parser.parse_args() - -# launch omniverse app -app_launcher = AppLauncher(args_cli) -simulation_app = app_launcher.app +args_cli.physics = args_cli.backend """Rest everything follows.""" import torch -import isaacsim.core.utils.prims as prim_utils - import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils -from isaaclab.assets import DeformableObject, DeformableObjectCfg -from isaaclab.sim import SimulationContext +from isaaclab.physics import PhysicsCfg + +if TYPE_CHECKING: + from isaaclab.assets import DeformableObject def design_scene(): """Designs the scene.""" + from isaaclab.assets import DeformableObject, DeformableObjectCfg + # Ground-plane cfg = sim_utils.GroundPlaneCfg() cfg.func("/World/defaultGroundPlane", cfg) @@ -52,67 +65,98 @@ def design_scene(): cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.8, 0.8, 0.8)) cfg.func("/World/Light", cfg) - # Create separate groups called "Origin1", "Origin2", "Origin3" - # Each group will have a robot in it + # Create a dictionary for the scene entities + scene_entities = {} + + # Create separate groups called "env_0", "env_1", ... + # Newton's scene loader requires the "env_\d+" naming convention to + # detect per-environment Xforms and replicate them as separate worlds. origins = [[0.25, 0.25, 0.0], [-0.25, 0.25, 0.0], [0.25, -0.25, 0.0], [-0.25, -0.25, 0.0]] for i, origin in enumerate(origins): - prim_utils.create_prim(f"/World/Origin{i}", "Xform", translation=origin) - - # Deformable Object + sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=origin) + + youngs_modulus = 1e5 + poissons_ratio = 0.4 + density = 500.0 + if args_cli.backend == "newton_vbd": + from isaaclab_newton.sim.schemas import NewtonDeformableBodyPropertiesCfg + from isaaclab_newton.sim.spawners.materials import NewtonDeformableBodyMaterialCfg + + deformable_props = NewtonDeformableBodyPropertiesCfg() + # Newton's VBD path skips the simulation mesh collider, so collision offsets do not apply + collision_props = None + physics_material = NewtonDeformableBodyMaterialCfg( + k_mu=youngs_modulus / (2.0 * (1.0 + poissons_ratio)), + k_lambda=youngs_modulus * poissons_ratio / ((1.0 + poissons_ratio) * (1.0 - 2.0 * poissons_ratio)), + density=density, + ) + else: + from isaaclab_physx.sim.schemas import PhysxCollisionCfg, PhysxDeformableBodyPropertiesCfg + from isaaclab_physx.sim.spawners.materials import PhysxDeformableBodyMaterialCfg + + deformable_props = PhysxDeformableBodyPropertiesCfg() + collision_props = [PhysxCollisionCfg(rest_offset=0.0, contact_offset=0.001)] + physics_material = PhysxDeformableBodyMaterialCfg( + poissons_ratio=poissons_ratio, youngs_modulus=youngs_modulus, density=density + ) + + # 3D Deformable Object cfg = DeformableObjectCfg( - prim_path="/World/Origin.*/Cube", + prim_path="/World/env_.*/Cube", spawn=sim_utils.MeshCuboidCfg( size=(0.2, 0.2, 0.2), - deformable_props=sim_utils.DeformableBodyPropertiesCfg(rest_offset=0.0, contact_offset=0.001), + deformable_props=deformable_props, + collision_props=collision_props, visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.5, 0.1, 0.0)), - physics_material=sim_utils.DeformableBodyMaterialCfg(poissons_ratio=0.4, youngs_modulus=1e5), + physics_material=physics_material, ), init_state=DeformableObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 1.0)), debug_vis=True, ) + cube_object = DeformableObject(cfg=cfg) + scene_entities["cube_object"] = cube_object # return the scene information - scene_entities = {"cube_object": cube_object} return scene_entities, origins -def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, DeformableObject], origins: torch.Tensor): +def run_simulator(sim: sim_utils.SimulationContext, entities: dict, origins: torch.Tensor): """Runs the simulation loop.""" # Extract scene entities # note: we only do this here for readability. In general, it is better to access the entities directly from # the dictionary. This dictionary is replaced by the InteractiveScene class in the next tutorial. - cube_object = entities["cube_object"] + cube_object: DeformableObject = entities["cube_object"] + # Define simulation stepping sim_dt = sim.get_physics_dt() sim_time = 0.0 count = 0 # Nodal kinematic targets of the deformable bodies - nodal_kinematic_target = cube_object.data.nodal_kinematic_target.clone() + nodal_kinematic_target = cube_object.data.nodal_kinematic_target.torch.clone() # Simulate physics - while simulation_app.is_running(): - # reset - if count % 250 == 0: + while sim.is_headless_or_exist_active_visualizer(): + # reset at start and after 3 seconds + if count % int(3.0 / sim_dt) == 0: # reset counters - sim_time = 0.0 count = 0 # reset the nodal state of the object - nodal_state = cube_object.data.default_nodal_state_w.clone() + nodal_state = cube_object.data.default_nodal_state_w.torch.clone() # apply random pose to the object pos_w = torch.rand(cube_object.num_instances, 3, device=sim.device) * 0.1 + origins quat_w = math_utils.random_orientation(cube_object.num_instances, device=sim.device) nodal_state[..., :3] = cube_object.transform_nodal_pos(nodal_state[..., :3], pos_w, quat_w) # write nodal state to simulation - cube_object.write_nodal_state_to_sim(nodal_state) + cube_object.write_nodal_state_to_sim_index(nodal_state) # 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) + cube_object.write_nodal_kinematic_target_to_sim_index(nodal_kinematic_target) # reset buffers cube_object.reset() @@ -121,13 +165,14 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, Deformab print("[INFO]: Resetting object state...") # update the kinematic target for cubes at index 0 and 3 + kinematic_cubes = [0, 3] # we slightly move the cube in the z-direction by picking the vertex at index 0 - nodal_kinematic_target[[0, 3], 0, 2] += 0.001 + nodal_kinematic_target[kinematic_cubes, 0, 2] += 0.2 * sim_dt # set vertex at index 0 to be kinematically constrained # 0: constrained, 1: free - nodal_kinematic_target[[0, 3], 0, 3] = 0.0 + nodal_kinematic_target[kinematic_cubes, 0, 3] = 0.0 # write kinematic target to simulation - cube_object.write_nodal_kinematic_target_to_sim(nodal_kinematic_target) + cube_object.write_nodal_kinematic_target_to_sim_index(nodal_kinematic_target) # write internal data to simulation cube_object.write_data_to_sim() @@ -138,31 +183,34 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, Deformab count += 1 # update buffers cube_object.update(sim_dt) - # print the root position - if count % 50 == 0: - print(f"Root position (in world): {cube_object.data.root_pos_w[:, :3]}") + + # print the root positions every second + if count % int(1.0 / sim_dt) == 0: + print(f"Time {sim_time:.2f}s: \tRoot position (in world): {cube_object.data.root_pos_w.torch[:, :3]}") def main(): """Main function.""" - # Load kit helper - sim_cfg = sim_utils.SimulationCfg(device=args_cli.device) - sim = SimulationContext(sim_cfg) - # Set main camera - sim.set_camera_view(eye=[3.0, 0.0, 1.0], target=[0.0, 0.0, 0.5]) - # Design scene - scene_entities, scene_origins = design_scene() - scene_origins = torch.tensor(scene_origins, device=sim.device) - # Play the simulator - sim.reset() - # Now we are ready! - print("[INFO]: Setup complete...") - # Run the simulator - run_simulator(sim, scene_entities, scene_origins) + with launch_simulation(cfg=PhysicsCfg(), launcher_args=args_cli) as physics_cfg: + if args_cli.backend == "newton_vbd": + physics_cfg.solver_cfg.iterations = 10 + physics_cfg.num_substeps = 4 + sim_cfg = sim_utils.SimulationCfg(dt=0.01, device=args_cli.device, physics=physics_cfg) + sim = sim_utils.SimulationContext(sim_cfg) + # Set main camera + sim.set_camera_view(eye=[2.0, 2.0, 2.0], target=[0.0, 0.0, 0.75]) + # Design scene + scene_entities, scene_origins = design_scene() + scene_origins = torch.tensor(scene_origins, device=sim.device) + # Play the simulator + sim.reset() + # Now we are ready! + print("[INFO]: Setup complete...") + # Run the simulator + run_simulator(sim, scene_entities, scene_origins) + print("[INFO]: Simulation complete...") if __name__ == "__main__": # run the main function main() - # close sim app - simulation_app.close() diff --git a/scripts/tutorials/01_assets/run_rigid_object.py b/scripts/tutorials/01_assets/run_rigid_object.py index 0ab19de7..266cac7b 100644 --- a/scripts/tutorials/01_assets/run_rigid_object.py +++ b/scripts/tutorials/01_assets/run_rigid_object.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,7 +9,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/01_assets/run_rigid_object.py + uv run python scripts/tutorials/01_assets/run_rigid_object.py """ @@ -35,8 +35,6 @@ import torch -import isaacsim.core.utils.prims as prim_utils - import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils from isaaclab.assets import RigidObject, RigidObjectCfg @@ -56,7 +54,7 @@ def design_scene(): # Each group will have a robot in it origins = [[0.25, 0.25, 0.0], [-0.25, 0.25, 0.0], [0.25, -0.25, 0.0], [-0.25, -0.25, 0.0]] for i, origin in enumerate(origins): - prim_utils.create_prim(f"/World/Origin{i}", "Xform", translation=origin) + sim_utils.create_prim(f"/World/Origin{i}", "Xform", translation=origin) # Rigid Object cone_cfg = RigidObjectCfg( @@ -96,15 +94,16 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, RigidObj sim_time = 0.0 count = 0 # reset root state - root_state = cone_object.data.default_root_state.clone() + root_pose = cone_object.data.default_root_pose.torch.clone() # sample a random position on a cylinder around the origins - root_state[:, :3] += origins - root_state[:, :3] += math_utils.sample_cylinder( + root_pose[:, :3] += origins + root_pose[:, :3] += math_utils.sample_cylinder( radius=0.1, h_range=(0.25, 0.5), size=cone_object.num_instances, device=cone_object.device ) # write root state to simulation - cone_object.write_root_pose_to_sim(root_state[:, :7]) - cone_object.write_root_velocity_to_sim(root_state[:, 7:]) + cone_object.write_root_pose_to_sim_index(root_pose=root_pose) + root_vel = cone_object.data.default_root_vel.torch.clone() + cone_object.write_root_velocity_to_sim_index(root_velocity=root_vel) # reset buffers cone_object.reset() print("----------------------------------------") @@ -120,7 +119,7 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, RigidObj cone_object.update(sim_dt) # print the root position if count % 50 == 0: - print(f"Root position (in world): {cone_object.data.root_pos_w}") + print(f"Root position (in world): {cone_object.data.root_pos_w.torch}") def main(): diff --git a/scripts/tutorials/01_assets/run_surface_gripper.py b/scripts/tutorials/01_assets/run_surface_gripper.py index 50a01431..3c321873 100644 --- a/scripts/tutorials/01_assets/run_surface_gripper.py +++ b/scripts/tutorials/01_assets/run_surface_gripper.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -8,7 +8,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/01_assets/run_surface_gripper.py --device=cpu + uv run python scripts/tutorials/01_assets/run_surface_gripper.py --device=cpu When running this script make sure the --device flag is set to cpu. This is because the surface gripper is currently only supported on the CPU. @@ -24,6 +24,8 @@ parser = argparse.ArgumentParser(description="Tutorial on spawning and interacting with a Surface Gripper.") # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) +# tutorials should open Kit visualizer by default +parser.set_defaults(visualizer=["kit"]) # parse the arguments args_cli = parser.parse_args() @@ -34,11 +36,11 @@ """Rest everything follows.""" import torch - -import isaacsim.core.utils.prims as prim_utils +import warp as wp +from isaaclab_physx.assets import SurfaceGripper, SurfaceGripperCfg import isaaclab.sim as sim_utils -from isaaclab.assets import Articulation, SurfaceGripper, SurfaceGripperCfg +from isaaclab.assets import Articulation from isaaclab.sim import SimulationContext ## @@ -60,9 +62,9 @@ def design_scene(): # Each group will have a robot in it origins = [[2.75, 0.0, 0.0], [-2.75, 0.0, 0.0]] # Origin 1 - prim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0]) + sim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0]) # Origin 2 - prim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) + sim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) # Articulation: First we define the robot config pick_and_place_robot_cfg = PICK_AND_PLACE_CFG.copy() @@ -108,14 +110,19 @@ def run_simulator( # 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] += origins - robot.write_root_pose_to_sim(root_state[:, :7]) - robot.write_root_velocity_to_sim(root_state[:, 7:]) + root_pose = robot.data.default_root_pose.torch.clone() + root_pose[:, :3] += origins + robot.write_root_pose_to_sim_index(root_pose=root_pose) + root_vel = robot.data.default_root_vel.torch.clone() + robot.write_root_velocity_to_sim_index(root_velocity=root_vel) # set joint positions with some noise - joint_pos, joint_vel = robot.data.default_joint_pos.clone(), robot.data.default_joint_vel.clone() + joint_pos, joint_vel = ( + robot.data.default_joint_pos.torch.clone(), + robot.data.default_joint_vel.torch.clone(), + ) joint_pos += torch.rand_like(joint_pos) * 0.1 - robot.write_joint_state_to_sim(joint_pos, joint_vel) + robot.write_joint_position_to_sim_index(position=joint_pos) + robot.write_joint_velocity_to_sim_index(velocity=joint_vel) # clear internal buffers robot.reset() print("[INFO]: Resetting robot state...") @@ -153,7 +160,8 @@ def run_simulator( # Print the gripper state print(f"[INFO]: Gripper state: {surface_gripper_state}") mapped_commands = [ - "Open" if state == -1 else "Closing" if state == 0 else "Closed" for state in surface_gripper_state.tolist() + "Open" if state == -1 else "Closing" if state == 0 else "Closed" + for state in wp.to_torch(surface_gripper_state).tolist() ] print(f"[INFO]: Mapped commands: {mapped_commands}") diff --git a/scripts/tutorials/02_scene/create_scene.py b/scripts/tutorials/02_scene/create_scene.py index 57d91a3e..36dce16c 100644 --- a/scripts/tutorials/02_scene/create_scene.py +++ b/scripts/tutorials/02_scene/create_scene.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -8,7 +8,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/02_scene/create_scene.py --num_envs 32 + uv run python scripts/tutorials/02_scene/create_scene.py --num_envs 32 """ @@ -81,22 +81,27 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): # 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_pose_to_sim(root_state[:, :7]) - robot.write_root_velocity_to_sim(root_state[:, 7:]) + root_pose = robot.data.default_root_pose.torch.clone() + root_pose[:, :3] += scene.env_origins + robot.write_root_pose_to_sim_index(root_pose=root_pose) + root_vel = robot.data.default_root_vel.torch.clone() + robot.write_root_velocity_to_sim_index(root_velocity=root_vel) # set joint positions with some noise - joint_pos, joint_vel = robot.data.default_joint_pos.clone(), robot.data.default_joint_vel.clone() + joint_pos, joint_vel = ( + robot.data.default_joint_pos.torch.clone(), + robot.data.default_joint_vel.torch.clone(), + ) joint_pos += torch.rand_like(joint_pos) * 0.1 - robot.write_joint_state_to_sim(joint_pos, joint_vel) + robot.write_joint_position_to_sim_index(position=joint_pos) + robot.write_joint_velocity_to_sim_index(velocity=joint_vel) # clear internal buffers scene.reset() print("[INFO]: Resetting robot state...") # Apply random action # -- generate random joint efforts - efforts = torch.randn_like(robot.data.joint_pos) * 5.0 + efforts = torch.randn_like(robot.data.joint_pos.torch) * 5.0 # -- apply action to the robot - robot.set_joint_effort_target(efforts) + robot.set_joint_effort_target_index(target=efforts) # -- write data to sim scene.write_data_to_sim() # Perform step diff --git a/scripts/tutorials/03_envs/create_cartpole_base_env.py b/scripts/tutorials/03_envs/create_cartpole_base_env.py index b48357ee..35da1550 100644 --- a/scripts/tutorials/03_envs/create_cartpole_base_env.py +++ b/scripts/tutorials/03_envs/create_cartpole_base_env.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,7 +9,7 @@ .. code-block:: bash - ./isaaclab.sh -p scripts/tutorials/03_envs/create_cartpole_base_env.py --num_envs 32 + uv run python scripts/tutorials/03_envs/create_cartpole_base_env.py --num_envs 32 """ @@ -36,6 +36,7 @@ """Rest everything follows.""" import math + import torch import isaaclab.envs.mdp as mdp @@ -45,8 +46,9 @@ from isaaclab.managers import ObservationTermCfg as ObsTerm from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass +from isaaclab.visualizers import VisualizerCfg -from isaaclab_tasks.manager_based.classic.cartpole.cartpole_env_cfg import CartpoleSceneCfg +from isaaclab_tasks.core.cartpole.cartpole_manager_env_cfg import CartpoleSceneCfg @configclass @@ -127,8 +129,7 @@ class CartpoleEnvCfg(ManagerBasedEnvCfg): def __post_init__(self): """Post initialization.""" # viewer settings - self.viewer.eye = [4.5, 0.0, 6.0] - self.viewer.lookat = [0.0, 0.0, 2.0] + self.sim.default_visualizer_cfg = VisualizerCfg(eye=(4.5, 0.0, 6.0), lookat=(0.0, 0.0, 2.0)) # step settings self.decimation = 4 # env step every 4 sim steps: 200Hz / 4 = 50Hz # simulation settings diff --git a/scripts/tutorials/03_envs/create_cube_base_env.py b/scripts/tutorials/03_envs/create_cube_base_env.py index 803d627f..08d203b9 100644 --- a/scripts/tutorials/03_envs/create_cube_base_env.py +++ b/scripts/tutorials/03_envs/create_cube_base_env.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -22,7 +22,7 @@ .. code-block:: bash # Run the script - ./isaaclab.sh -p scripts/tutorials/03_envs/create_cube_base_env.py --num_envs 32 + uv run python scripts/tutorials/03_envs/create_cube_base_env.py --num_envs 32 """ @@ -56,14 +56,14 @@ import isaaclab.sim as sim_utils from isaaclab.assets import AssetBaseCfg, RigidObject, RigidObjectCfg from isaaclab.envs import ManagerBasedEnv, ManagerBasedEnvCfg -from isaaclab.managers import ActionTerm, ActionTermCfg +from isaaclab.managers import ActionTerm, ActionTermCfg, SceneEntityCfg from isaaclab.managers import EventTermCfg as EventTerm from isaaclab.managers import ObservationGroupCfg as ObsGroup from isaaclab.managers import ObservationTermCfg as ObsTerm -from isaaclab.managers import SceneEntityCfg from isaaclab.scene import InteractiveSceneCfg from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass +from isaaclab.visualizers import VisualizerCfg ## # Custom action term @@ -129,11 +129,11 @@ def process_actions(self, actions: torch.Tensor): def apply_actions(self): # implement a PD controller to track the target position - pos_error = self._processed_actions - (self._asset.data.root_pos_w - self._env.scene.env_origins) - vel_error = -self._asset.data.root_lin_vel_w + pos_error = self._processed_actions - (self._asset.data.root_pos_w.torch - self._env.scene.env_origins) + vel_error = -self._asset.data.root_lin_vel_w.torch # set velocity targets self._vel_command[:, :3] = self.p_gain * pos_error + self.d_gain * vel_error - self._asset.write_root_velocity_to_sim(self._vel_command) + self._asset.write_root_velocity_to_sim_index(root_velocity=self._vel_command) @configclass @@ -158,7 +158,7 @@ def base_position(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg) -> torch.Tens """Root linear velocity in the asset's root frame.""" # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] - return asset.data.root_pos_w - env.scene.env_origins + return asset.data.root_pos_w.torch - env.scene.env_origins ## @@ -306,8 +306,7 @@ def __post_init__(self): self.sim.render_interval = 2 # render interval should be a multiple of decimation self.sim.device = args_cli.device # viewer settings - self.viewer.eye = (5.0, 5.0, 5.0) - self.viewer.lookat = (0.0, 0.0, 2.0) + self.sim.default_visualizer_cfg = VisualizerCfg(eye=(5.0, 5.0, 5.0), lookat=(0.0, 0.0, 2.0)) def main(): @@ -337,7 +336,7 @@ def main(): # step env obs, _ = env.step(target_position) # print mean squared position error between target and current position - error = torch.norm(obs["policy"] - target_position).mean().item() + error = torch.linalg.norm(obs["policy"] - target_position).mean().item() print(f"[Step: {count:04d}]: Mean position error: {error:.4f}") # update counter count += 1 diff --git a/scripts/tutorials/03_envs/create_quadruped_base_env.py b/scripts/tutorials/03_envs/create_quadruped_base_env.py index 5aac02a8..151c213a 100644 --- a/scripts/tutorials/03_envs/create_quadruped_base_env.py +++ b/scripts/tutorials/03_envs/create_quadruped_base_env.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -13,7 +13,7 @@ .. code-block:: bash # Run the script - ./isaaclab.sh -p scripts/tutorials/03_envs/create_quadruped_base_env.py --num_envs 32 + uv run python scripts/tutorials/03_envs/create_quadruped_base_env.py --num_envs 32 """ @@ -54,7 +54,7 @@ from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR, check_file_path, read_file -from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise +from isaaclab.utils.noise import UniformNoiseCfg as Unoise ## # Pre-defined configs diff --git a/scripts/tutorials/03_envs/policy_inference_in_usd.py b/scripts/tutorials/03_envs/policy_inference_in_usd.py index cab84e62..c9f1e3ce 100644 --- a/scripts/tutorials/03_envs/policy_inference_in_usd.py +++ b/scripts/tutorials/03_envs/policy_inference_in_usd.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -7,12 +7,12 @@ This script demonstrates policy inference in a prebuilt USD environment. In this example, we use a locomotion policy to control the H1 robot. The robot was trained -using Isaac-Velocity-Rough-H1-v0. The robot is commanded to move forward at a constant velocity. +using Isaac-Velocity-Rough-H1. The robot is commanded to move forward at a constant velocity. .. code-block:: bash # Run the script - ./isaaclab.sh -p scripts/tutorials/03_envs/policy_inference_in_usd.py --checkpoint /path/to/jit/checkpoint.pt + uv run python scripts/tutorials/03_envs/policy_inference_in_usd.py --checkpoint /path/to/jit/checkpoint.pt """ @@ -29,45 +29,41 @@ # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) -# parse the arguments -args_cli = parser.parse_args() +# parse the arguments, forwarding unrecognized ones as Hydra-style task config overrides +args_cli, hydra_overrides = parser.parse_known_args() # launch omniverse app app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app """Rest everything follows.""" -import io import os -import torch -import omni +import torch from isaaclab.envs import ManagerBasedRLEnv from isaaclab.terrains import TerrainImporterCfg -from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, read_file -from isaaclab_tasks.manager_based.locomotion.velocity.config.h1.rough_env_cfg import H1RoughEnvCfg_PLAY +from isaaclab_tasks.utils import parse_env_cfg def main(): """Main function.""" # load the trained jit policy policy_path = os.path.abspath(args_cli.checkpoint) - file_content = omni.client.read_file(policy_path)[2] - file = io.BytesIO(memoryview(file_content).tobytes()) + file = read_file(policy_path) policy = torch.jit.load(file, map_location=args_cli.device) # setup environment - env_cfg = H1RoughEnvCfg_PLAY() - env_cfg.scene.num_envs = 1 + env_cfg = parse_env_cfg("Isaac-Velocity-Rough-H1", device=args_cli.device, num_envs=1, overrides=hydra_overrides) + env_cfg.play_mode() env_cfg.curriculum = None env_cfg.scene.terrain = TerrainImporterCfg( prim_path="/World/ground", terrain_type="usd", usd_path=f"{ISAAC_NUCLEUS_DIR}/Environments/Simple_Warehouse/warehouse.usd", ) - env_cfg.sim.device = args_cli.device if args_cli.device == "cpu": env_cfg.sim.use_fabric = False diff --git a/scripts/tutorials/03_envs/run_cartpole_rl_env.py b/scripts/tutorials/03_envs/run_cartpole_rl_env.py index 87cee28a..2f36a26b 100644 --- a/scripts/tutorials/03_envs/run_cartpole_rl_env.py +++ b/scripts/tutorials/03_envs/run_cartpole_rl_env.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -8,7 +8,10 @@ .. code-block:: bash - ./isaaclab.sh -p scripts/tutorials/03_envs/run_cartpole_rl_env.py --num_envs 32 + uv run python scripts/tutorials/03_envs/run_cartpole_rl_env.py --num_envs 32 + +Trailing ``key=value`` arguments (e.g. ``physics=isaacsim_physx``) are forwarded as Hydra-style +overrides to the task configuration; see :func:`~isaaclab_tasks.utils.parse_env_cfg`. """ @@ -24,8 +27,10 @@ # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) -# parse the arguments -args_cli = parser.parse_args() +# tutorials should open Kit visualizer by default +parser.set_defaults(visualizer=["kit"]) +# parse the arguments, forwarding unrecognized ones as Hydra-style task config overrides +args_cli, hydra_overrides = parser.parse_known_args() # launch omniverse app app_launcher = AppLauncher(args_cli) @@ -37,15 +42,15 @@ from isaaclab.envs import ManagerBasedRLEnv -from isaaclab_tasks.manager_based.classic.cartpole.cartpole_env_cfg import CartpoleEnvCfg +from isaaclab_tasks.utils import parse_env_cfg def main(): """Main function.""" # create environment configuration - env_cfg = CartpoleEnvCfg() - env_cfg.scene.num_envs = args_cli.num_envs - env_cfg.sim.device = args_cli.device + env_cfg = parse_env_cfg( + "Isaac-Cartpole", device=args_cli.device, num_envs=args_cli.num_envs, overrides=hydra_overrides + ) # setup RL environment env = ManagerBasedRLEnv(cfg=env_cfg) diff --git a/scripts/tutorials/04_sensors/add_sensors_on_robot.py b/scripts/tutorials/04_sensors/add_sensors_on_robot.py index c877566b..7eb6e9ed 100644 --- a/scripts/tutorials/04_sensors/add_sensors_on_robot.py +++ b/scripts/tutorials/04_sensors/add_sensors_on_robot.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -15,7 +15,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/04_sensors/add_sensors_on_robot.py --enable_cameras + uv run python scripts/tutorials/04_sensors/add_sensors_on_robot.py --viz kit """ @@ -111,25 +111,27 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): # 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_pose_to_sim(root_state[:, :7]) - scene["robot"].write_root_velocity_to_sim(root_state[:, 7:]) + root_pose = scene["robot"].data.default_root_pose.torch.clone() + root_pose[:, :3] += scene.env_origins + scene["robot"].write_root_pose_to_sim_index(root_pose=root_pose) + root_vel = scene["robot"].data.default_root_vel.torch.clone() + scene["robot"].write_root_velocity_to_sim_index(root_velocity=root_vel) # set joint positions with some noise joint_pos, joint_vel = ( - scene["robot"].data.default_joint_pos.clone(), - scene["robot"].data.default_joint_vel.clone(), + scene["robot"].data.default_joint_pos.torch.clone(), + scene["robot"].data.default_joint_vel.torch.clone(), ) joint_pos += torch.rand_like(joint_pos) * 0.1 - scene["robot"].write_joint_state_to_sim(joint_pos, joint_vel) + scene["robot"].write_joint_position_to_sim_index(position=joint_pos) + scene["robot"].write_joint_velocity_to_sim_index(velocity=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 + targets = scene["robot"].data.default_joint_pos.torch # -- apply action to the robot - scene["robot"].set_joint_position_target(targets) + scene["robot"].set_joint_position_target_index(target=targets) # -- write data to sim scene.write_data_to_sim() # perform step @@ -147,10 +149,13 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): print("Received shape of depth image: ", scene["camera"].data.output["distance_to_image_plane"].shape) print("-------------------------------") print(scene["height_scanner"]) - print("Received max height value: ", torch.max(scene["height_scanner"].data.ray_hits_w[..., -1]).item()) + print( + "Received max height value: ", + torch.max(scene["height_scanner"].data.ray_hits_w.torch[..., -1]).item(), + ) print("-------------------------------") print(scene["contact_forces"]) - print("Received max contact force of: ", torch.max(scene["contact_forces"].data.net_forces_w).item()) + print("Received max contact force of: ", torch.max(scene["contact_forces"].data.net_normal_forces_w).item()) def main(): diff --git a/scripts/tutorials/04_sensors/run_frame_transformer.py b/scripts/tutorials/04_sensors/run_frame_transformer.py index 41890e9d..64c7772d 100644 --- a/scripts/tutorials/04_sensors/run_frame_transformer.py +++ b/scripts/tutorials/04_sensors/run_frame_transformer.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,7 +9,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/04_sensors/run_frame_transformer.py + uv run python scripts/tutorials/04_sensors/run_frame_transformer.py --viz kit """ @@ -27,15 +27,20 @@ args_cli = parser.parse_args() # launch omniverse app -app_launcher = AppLauncher(headless=args_cli.headless) +app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app """Rest everything follows.""" import math + import torch -import isaacsim.util.debug_draw._debug_draw as omni_debug_draw +from isaaclab.sim.utils import enable_extension + +enable_extension("isaacsim.util.debug_draw") + +from isaacsim.util.debug_draw import _debug_draw as omni_debug_draw import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils @@ -50,6 +55,9 @@ ## from isaaclab_assets.robots.anymal import ANYMAL_C_CFG # isort:skip +ROBOT_PRIM_PATH = "/World/envs/env_0/Robot" +ROBOT_PRIM_PATH_EXPR = "/World/envs/env_.*/Robot" + def define_sensor() -> FrameTransformer: """Defines the FrameTransformer sensor to add to the scene.""" @@ -59,11 +67,11 @@ def define_sensor() -> FrameTransformer: # Example using .* to get full body + LF_FOOT frame_transformer_cfg = FrameTransformerCfg( - prim_path="/World/Robot/base", + prim_path=f"{ROBOT_PRIM_PATH_EXPR}/base", target_frames=[ - FrameTransformerCfg.FrameCfg(prim_path="/World/Robot/.*"), + FrameTransformerCfg.FrameCfg(prim_path=f"{ROBOT_PRIM_PATH_EXPR}/.*"), FrameTransformerCfg.FrameCfg( - prim_path="/World/Robot/LF_SHANK", + prim_path=f"{ROBOT_PRIM_PATH_EXPR}/LF_SHANK", name="LF_FOOT_USER", offset=OffsetCfg(pos=tuple(pos_offset.tolist()), rot=tuple(rot_offset[0].tolist())), ), @@ -85,7 +93,7 @@ def design_scene() -> dict: cfg = sim_utils.DistantLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) cfg.func("/World/Light", cfg) # -- Robot - robot = Articulation(ANYMAL_C_CFG.replace(prim_path="/World/Robot")) + robot = Articulation(ANYMAL_C_CFG.replace(prim_path=ROBOT_PRIM_PATH)) # -- Sensors frame_transformer = define_sensor() @@ -122,7 +130,7 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): # Simulate physics while simulation_app.is_running(): # perform this loop at policy control freq (50 Hz) - robot.set_joint_position_target(robot.data.default_joint_pos.clone()) + robot.set_joint_position_target_index(target=robot.data.default_joint_pos.torch.clone()) robot.write_data_to_sim() # perform step sim.step() @@ -145,10 +153,10 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): print(f"Displaying Frame ID {frame_index}: {frame_names[frame_index]}") # visualize frame - source_pos = frame_transformer.data.source_pos_w - source_quat = frame_transformer.data.source_quat_w - target_pos = frame_transformer.data.target_pos_w[:, frame_index] - target_quat = frame_transformer.data.target_quat_w[:, frame_index] + source_pos = frame_transformer.data.source_pos_w.torch + source_quat = frame_transformer.data.source_quat_w.torch + target_pos = frame_transformer.data.target_pos_w.torch[:, frame_index] + target_quat = frame_transformer.data.target_quat_w.torch[:, frame_index] # draw the frames transform_visualizer.visualize( torch.cat([source_pos, target_pos], dim=0), torch.cat([source_quat, target_quat], dim=0) diff --git a/scripts/tutorials/04_sensors/run_ray_caster.py b/scripts/tutorials/04_sensors/run_ray_caster.py index 9916fa97..7287df3a 100644 --- a/scripts/tutorials/04_sensors/run_ray_caster.py +++ b/scripts/tutorials/04_sensors/run_ray_caster.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,7 +9,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/04_sensors/run_ray_caster.py + uv run python scripts/tutorials/04_sensors/run_ray_caster.py --viz kit """ @@ -33,8 +33,6 @@ import torch -import isaacsim.core.utils.prims as prim_utils - import isaaclab.sim as sim_utils from isaaclab.assets import RigidObject, RigidObjectCfg from isaaclab.sensors.ray_caster import RayCaster, RayCasterCfg, patterns @@ -71,7 +69,7 @@ def design_scene() -> dict: # Each group will have a robot in it origins = [[0.25, 0.25, 0.0], [-0.25, 0.25, 0.0], [0.25, -0.25, 0.0], [-0.25, -0.25, 0.0]] for i, origin in enumerate(origins): - prim_utils.create_prim(f"/World/Origin{i}", "Xform", translation=origin) + sim_utils.create_prim(f"/World/Origin{i}", "Xform", translation=origin) # -- Balls cfg = RigidObjectCfg( prim_path="/World/Origin.*/ball", @@ -99,8 +97,9 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): balls: RigidObject = scene_entities["balls"] # define an initial position of the sensor - ball_default_state = balls.data.default_root_state.clone() - ball_default_state[:, :3] = torch.rand_like(ball_default_state[:, :3]) * 10 + ball_default_pose = balls.data.default_root_pose.torch.clone() + ball_default_pose[:, :3] = torch.rand_like(ball_default_pose[:, :3]) * 10 + ball_default_vel = balls.data.default_root_vel.torch.clone() # Create a counter for resetting the scene step_count = 0 @@ -109,8 +108,8 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): # Reset the scene if step_count % 250 == 0: # reset the balls - balls.write_root_pose_to_sim(ball_default_state[:, :7]) - balls.write_root_velocity_to_sim(ball_default_state[:, 7:]) + balls.write_root_pose_to_sim_index(root_pose=ball_default_pose) + balls.write_root_velocity_to_sim_index(root_velocity=ball_default_vel) # reset the sensor ray_caster.reset() # reset the counter @@ -120,7 +119,7 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): # Update the ray-caster with Timer( f"Ray-caster update with {4} x {ray_caster.num_rays} rays with max height of" - f" {torch.max(ray_caster.data.pos_w).item():.2f}" + f" {torch.max(ray_caster.data.pos_w.torch).item():.2f}" ): ray_caster.update(dt=sim.get_physics_dt(), force_recompute=True) # Update counter diff --git a/scripts/tutorials/04_sensors/run_ray_caster_camera.py b/scripts/tutorials/04_sensors/run_ray_caster_camera.py index 3cfe3905..755106a5 100644 --- a/scripts/tutorials/04_sensors/run_ray_caster_camera.py +++ b/scripts/tutorials/04_sensors/run_ray_caster_camera.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -11,7 +11,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/04_sensors/run_ray_caster_camera.py + uv run python scripts/tutorials/04_sensors/run_ray_caster_camera.py --viz kit """ @@ -36,10 +36,9 @@ """Rest everything follows.""" import os -import torch +from typing import Any -import isaacsim.core.utils.prims as prim_utils -import omni.replicator.core as rep +import torch import isaaclab.sim as sim_utils from isaaclab.sensors.ray_caster import RayCasterCamera, RayCasterCameraCfg, patterns @@ -53,12 +52,12 @@ def define_sensor() -> RayCasterCamera: # Camera base frames # In contras to the USD camera, we associate the sensor to the prims at these locations. # This means that parent prim of the sensor is the prim at this location. - prim_utils.create_prim("/World/Origin_00/CameraSensor", "Xform") - prim_utils.create_prim("/World/Origin_01/CameraSensor", "Xform") + sim_utils.create_prim("/World/Origin_00/CameraSensor", "Xform") + sim_utils.create_prim("/World/Origin_01/CameraSensor", "Xform") # Setup camera sensor camera_cfg = RayCasterCameraCfg( - prim_path="/World/Origin_.*/CameraSensor", + prim_path="/World/Origin_[^/]+/CameraSensor", mesh_prim_paths=["/World/ground"], update_period=0.1, offset=RayCasterCameraCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), @@ -98,9 +97,14 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): # extract entities for simplified notation camera: RayCasterCamera = scene_entities["camera"] - # Create replicator writer - output_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "output", "ray_caster_camera") - rep_writer = rep.BasicWriter(output_dir=output_dir, frame_padding=3) + # Create the Replicator writer only when saving. The ray-cast camera itself + # is Warp-based and does not require Replicator or RTX rendering extensions. + rep_writer: Any | None = None + if args_cli.save: + import omni.replicator.core as rep + + output_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "output", "ray_caster_camera") + rep_writer = rep.BasicWriter(output_dir=output_dir, frame_padding=3) # Set pose: There are two ways to set the pose of the camera. # -- Option-1: Set pose using view @@ -132,18 +136,17 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): 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 = {"annotators": {}} - for key, data, info in zip(single_cam_data.keys(), single_cam_data.values(), single_cam_info.values()): + for key, data in single_cam_data.items(): + info = camera.data.info.get(key) if info is not None: rep_output["annotators"][key] = {"render_product": {"data": data, **info}} else: rep_output["annotators"][key] = {"render_product": {"data": data}} # Save images rep_output["trigger_outputs"] = {"on_time": camera.frame[camera_index]} + assert rep_writer is not None rep_writer.write(rep_output) # Pointcloud in world frame diff --git a/scripts/tutorials/04_sensors/run_usd_camera.py b/scripts/tutorials/04_sensors/run_usd_camera.py index 1d360231..7fa75adb 100644 --- a/scripts/tutorials/04_sensors/run_usd_camera.py +++ b/scripts/tutorials/04_sensors/run_usd_camera.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -12,10 +12,10 @@ .. code-block:: bash # Usage with GUI - ./isaaclab.sh -p scripts/tutorials/04_sensors/run_usd_camera.py --enable_cameras + uv run python scripts/tutorials/04_sensors/run_usd_camera.py --viz kit - # Usage with headless - ./isaaclab.sh -p scripts/tutorials/04_sensors/run_usd_camera.py --headless --enable_cameras + # Usage with no visualizer + uv run python scripts/tutorials/04_sensors/run_usd_camera.py """ @@ -53,6 +53,8 @@ AppLauncher.add_app_launcher_args(parser) # parse the arguments args_cli = parser.parse_args() +# Camera sensors require the rendering extensions in headless and viewport-free launches. +args_cli.enable_cameras = True # launch omniverse app app_launcher = AppLauncher(args_cli) @@ -60,12 +62,13 @@ """Rest everything follows.""" -import numpy as np import os import random + +import numpy as np import torch +from isaaclab_physx.renderers import IsaacRtxRendererCfg -import isaacsim.core.utils.prims as prim_utils import omni.replicator.core as rep import isaaclab.sim as sim_utils @@ -82,10 +85,10 @@ def define_sensor() -> Camera: # Setup camera sensor # In contrast to the ray-cast camera, we spawn the prim at these locations. # This means the camera sensor will be attached to these prims. - prim_utils.create_prim("/World/Origin_00", "Xform") - prim_utils.create_prim("/World/Origin_01", "Xform") + sim_utils.create_prim("/World/Origin_00", "Xform") + sim_utils.create_prim("/World/Origin_01", "Xform") camera_cfg = CameraCfg( - prim_path="/World/Origin_.*/CameraSensor", + prim_path="/World/Origin_[^/]+/CameraSensor", update_period=0, height=480, width=640, @@ -94,12 +97,14 @@ def define_sensor() -> Camera: "distance_to_image_plane", "normals", "semantic_segmentation", - "instance_segmentation_fast", + "instance_segmentation", "instance_id_segmentation_fast", ], - colorize_semantic_segmentation=True, - colorize_instance_id_segmentation=True, - colorize_instance_segmentation=True, + renderer_cfg=IsaacRtxRendererCfg( + colorize_semantic_segmentation=True, + colorize_instance_id_segmentation=True, + colorize_instance_segmentation=True, + ), spawn=sim_utils.PinholeCameraCfg( focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 1.0e5) ), @@ -124,7 +129,7 @@ def design_scene() -> dict: scene_entities = {} # Xform to hold objects - prim_utils.create_prim("/World/Objects", "Xform") + sim_utils.create_prim("/World/Objects", "Xform") # Random objects for i in range(8): # sample random position @@ -173,9 +178,9 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): rep_writer = rep.BasicWriter( output_dir=output_dir, frame_padding=0, - colorize_instance_id_segmentation=camera.cfg.colorize_instance_id_segmentation, - colorize_instance_segmentation=camera.cfg.colorize_instance_segmentation, - colorize_semantic_segmentation=camera.cfg.colorize_semantic_segmentation, + colorize_instance_id_segmentation=camera.cfg.renderer_cfg.colorize_instance_id_segmentation, + colorize_instance_segmentation=camera.cfg.renderer_cfg.colorize_instance_segmentation, + colorize_semantic_segmentation=camera.cfg.renderer_cfg.colorize_semantic_segmentation, ) # Camera positions, targets, orientations @@ -196,7 +201,7 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): camera_index = args_cli.camera_id # Create the markers for the --draw option outside of is_running() loop - if sim.has_gui() and args_cli.draw: + if sim.get_setting("/isaaclab/has_gui") and args_cli.draw: cfg = RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/CameraPointCloud") cfg.markers["hit"].radius = 0.002 pc_markers = VisualizationMarkers(cfg) @@ -218,8 +223,8 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): print("Received shape of normals : ", camera.data.output["normals"].shape) if "semantic_segmentation" in camera.data.output.keys(): print("Received shape of semantic segm. : ", camera.data.output["semantic_segmentation"].shape) - if "instance_segmentation_fast" in camera.data.output.keys(): - print("Received shape of instance segm. : ", camera.data.output["instance_segmentation_fast"].shape) + if "instance_segmentation" in camera.data.output.keys(): + print("Received shape of instance segm. : ", camera.data.output["instance_segmentation"].shape) if "instance_id_segmentation_fast" in camera.data.output.keys(): print("Received shape of instance id segm.: ", camera.data.output["instance_id_segmentation_fast"].shape) print("-------------------------------") @@ -232,12 +237,10 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): {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 = {"annotators": {}} - for key, data, info in zip(single_cam_data.keys(), single_cam_data.values(), single_cam_info.values()): + for key, data in single_cam_data.items(): + info = camera.data.info.get(key) if info is not None: rep_output["annotators"][key] = {"render_product": {"data": data, **info}} else: @@ -248,7 +251,11 @@ def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict): rep_writer.write(rep_output) # Draw pointcloud if there is a GUI and --draw has been passed - if sim.has_gui() and args_cli.draw and "distance_to_image_plane" in camera.data.output.keys(): + if ( + sim.get_setting("/isaaclab/has_gui") + and args_cli.draw + and "distance_to_image_plane" in camera.data.output.keys() + ): # Derive pointcloud from camera at camera_index pointcloud = create_pointcloud_from_depth( intrinsic_matrix=camera.data.intrinsic_matrices[camera_index], diff --git a/scripts/tutorials/05_controllers/run_diff_ik.py b/scripts/tutorials/05_controllers/run_diff_ik.py index 180b4c48..7008ad0d 100644 --- a/scripts/tutorials/05_controllers/run_diff_ik.py +++ b/scripts/tutorials/05_controllers/run_diff_ik.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -12,7 +12,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/05_controllers/run_diff_ik.py + uv run python scripts/tutorials/05_controllers/run_diff_ik.py """ @@ -105,11 +105,11 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): ee_marker = VisualizationMarkers(frame_marker_cfg.replace(prim_path="/Visuals/ee_current")) goal_marker = VisualizationMarkers(frame_marker_cfg.replace(prim_path="/Visuals/ee_goal")) - # Define goals for the arm + # Define goals for the arm (x,y,z,qx,qy,qz,qw) ee_goals = [ - [0.5, 0.5, 0.7, 0.707, 0, 0.707, 0], - [0.5, -0.4, 0.6, 0.707, 0.707, 0.0, 0.0], - [0.5, 0, 0.5, 0.0, 1.0, 0.0, 0.0], + [0.5, 0.5, 0.7, 0, 0.707, 0, 0.707], + [0.5, -0.4, 0.6, 0.707, 0, 0, 0.707], + [0.5, 0, 0.5, 1.0, 0.0, 0.0, 0.0], ] ee_goals = torch.tensor(ee_goals, device=sim.device) # Track the given command @@ -145,9 +145,10 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): # reset time count = 0 # reset joint state - joint_pos = robot.data.default_joint_pos.clone() - joint_vel = robot.data.default_joint_vel.clone() - robot.write_joint_state_to_sim(joint_pos, joint_vel) + joint_pos = robot.data.default_joint_pos.torch.clone() + joint_vel = robot.data.default_joint_vel.torch.clone() + robot.write_joint_position_to_sim_index(position=joint_pos) + robot.write_joint_velocity_to_sim_index(velocity=joint_vel) robot.reset() # reset actions ik_commands[:] = ee_goals[current_goal_idx] @@ -158,11 +159,14 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): # change goal current_goal_idx = (current_goal_idx + 1) % len(ee_goals) else: - # obtain quantities from simulation - jacobian = robot.root_physx_view.get_jacobians()[:, ee_jacobi_idx, :, robot_entity_cfg.joint_ids] - ee_pose_w = robot.data.body_pose_w[:, robot_entity_cfg.body_ids[0]] - root_pose_w = robot.data.root_pose_w - joint_pos = robot.data.joint_pos[:, robot_entity_cfg.joint_ids] + # obtain quantities from simulation. The Jacobian DoF axis prepends + # ``num_base_dofs`` floating-base columns (0 for fixed-base, 6 for + # floating-base); shift the actuated-joint ids accordingly. + jacobi_joint_ids = [j + robot.num_base_dofs for j in robot_entity_cfg.joint_ids] + jacobian = robot.data.body_link_jacobian_w.torch[:, ee_jacobi_idx, :, jacobi_joint_ids] + ee_pose_w = robot.data.body_pose_w.torch[:, robot_entity_cfg.body_ids[0]] + root_pose_w = robot.data.root_pose_w.torch + joint_pos = robot.data.joint_pos.torch[:, robot_entity_cfg.joint_ids] # compute frame in root frame ee_pos_b, ee_quat_b = subtract_frame_transforms( root_pose_w[:, 0:3], root_pose_w[:, 3:7], ee_pose_w[:, 0:3], ee_pose_w[:, 3:7] @@ -171,7 +175,7 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): joint_pos_des = diff_ik_controller.compute(ee_pos_b, ee_quat_b, jacobian, joint_pos) # apply actions - robot.set_joint_position_target(joint_pos_des, joint_ids=robot_entity_cfg.joint_ids) + robot.set_joint_position_target_index(target=joint_pos_des, joint_ids=robot_entity_cfg.joint_ids) scene.write_data_to_sim() # perform step sim.step() @@ -181,7 +185,7 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): scene.update(sim_dt) # obtain quantities from simulation - ee_pose_w = robot.data.body_state_w[:, robot_entity_cfg.body_ids[0], 0:7] + ee_pose_w = robot.data.body_state_w.torch[:, robot_entity_cfg.body_ids[0], 0:7] # update marker positions ee_marker.visualize(ee_pose_w[:, 0:3], ee_pose_w[:, 3:7]) goal_marker.visualize(ik_commands[:, 0:3] + scene.env_origins, ik_commands[:, 3:7]) diff --git a/scripts/tutorials/05_controllers/run_osc.py b/scripts/tutorials/05_controllers/run_osc.py index 7578fcf9..5089ec78 100644 --- a/scripts/tutorials/05_controllers/run_osc.py +++ b/scripts/tutorials/05_controllers/run_osc.py @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). -# All Rights Reserved. +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -12,7 +12,7 @@ .. code-block:: bash # Usage - ./isaaclab.sh -p scripts/tutorials/05_controllers/run_osc.py + uv run python scripts/tutorials/05_controllers/run_osc.py """ @@ -86,7 +86,7 @@ class SceneCfg(InteractiveSceneCfg): activate_contact_sensors=True, ), init_state=AssetBaseCfg.InitialStateCfg( - pos=(0.6 + 0.085, 0.0, 0.3), rot=(0.9238795325, 0.0, -0.3826834324, 0.0) + pos=(0.6 + 0.085, 0.0, 0.3), rot=(0.0, -0.3826834324, 0.0, 0.9238795325) ), ) @@ -144,12 +144,12 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): ee_marker = VisualizationMarkers(frame_marker_cfg.replace(prim_path="/Visuals/ee_current")) goal_marker = VisualizationMarkers(frame_marker_cfg.replace(prim_path="/Visuals/ee_goal")) - # Define targets for the arm + # Define targets for the arm (x,y,z,qx,qy,qz,qw) ee_goal_pose_set_tilted_b = torch.tensor( [ - [0.6, 0.15, 0.3, 0.0, 0.92387953, 0.0, 0.38268343], - [0.6, -0.3, 0.3, 0.0, 0.92387953, 0.0, 0.38268343], - [0.8, 0.0, 0.5, 0.0, 0.92387953, 0.0, 0.38268343], + [0.6, 0.15, 0.3, 0.0, 0.38268343, 0.0, 0.92387953], + [0.6, -0.3, 0.3, 0.0, 0.38268343, 0.0, 0.92387953], + [0.8, 0.0, 0.5, 0.0, 0.38268343, 0.0, 0.92387953], ], device=sim.device, ) @@ -179,7 +179,7 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): robot.update(dt=sim_dt) # Get the center of the robot soft joint limits - joint_centers = torch.mean(robot.data.soft_joint_pos_limits[:, arm_joint_ids, :], dim=-1) + joint_centers = torch.mean(robot.data.soft_joint_pos_limits.torch[:, arm_joint_ids, :], dim=-1) # get the updated states ( @@ -213,10 +213,11 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): # reset every 500 steps if count % 500 == 0: # reset joint state to default - default_joint_pos = robot.data.default_joint_pos.clone() - default_joint_vel = robot.data.default_joint_vel.clone() - robot.write_joint_state_to_sim(default_joint_pos, default_joint_vel) - robot.set_joint_effort_target(zero_joint_efforts) # Set zero torques in the initial step + default_joint_pos = robot.data.default_joint_pos.torch.clone() + default_joint_vel = robot.data.default_joint_vel.torch.clone() + robot.write_joint_position_to_sim_index(position=default_joint_pos) + robot.write_joint_velocity_to_sim_index(velocity=default_joint_vel) + robot.set_joint_effort_target_index(target=zero_joint_efforts) # Set zero torques in the initial step robot.write_data_to_sim() robot.reset() # reset contact sensor @@ -260,7 +261,7 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): nullspace_joint_pos_target=joint_centers, ) # apply actions - robot.set_joint_effort_target(joint_efforts, joint_ids=arm_joint_ids) + robot.set_joint_effort_target_index(target=joint_efforts, joint_ids=arm_joint_ids) robot.write_data_to_sim() # update marker positions @@ -313,31 +314,35 @@ def update_states( """ # obtain dynamics related quantities from simulation ee_jacobi_idx = ee_frame_idx - 1 - jacobian_w = robot.root_physx_view.get_jacobians()[:, ee_jacobi_idx, :, arm_joint_ids] - mass_matrix = robot.root_physx_view.get_generalized_mass_matrices()[:, arm_joint_ids, :][:, :, arm_joint_ids] - gravity = robot.root_physx_view.get_gravity_compensation_forces()[:, arm_joint_ids] + # The J / M / g DoF axis prepends ``num_base_dofs`` floating-base columns + # (0 for fixed-base, 6 for floating-base); shift the actuated-joint ids by + # ``num_base_dofs`` to address the actuated-joint columns directly. + jacobi_joint_ids = [j + robot.num_base_dofs for j in arm_joint_ids] + jacobian_w = robot.data.body_link_jacobian_w.torch[:, ee_jacobi_idx, :, jacobi_joint_ids] + mass_matrix = robot.data.mass_matrix.torch[:, jacobi_joint_ids, :][:, :, jacobi_joint_ids] + gravity = robot.data.gravity_compensation_forces.torch[:, jacobi_joint_ids] # Convert the Jacobian from world to root frame jacobian_b = jacobian_w.clone() - root_rot_matrix = matrix_from_quat(quat_inv(robot.data.root_quat_w)) + root_rot_matrix = matrix_from_quat(quat_inv(robot.data.root_quat_w.torch)) jacobian_b[:, :3, :] = torch.bmm(root_rot_matrix, jacobian_b[:, :3, :]) jacobian_b[:, 3:, :] = torch.bmm(root_rot_matrix, jacobian_b[:, 3:, :]) # Compute current pose of the end-effector - root_pos_w = robot.data.root_pos_w - root_quat_w = robot.data.root_quat_w - ee_pos_w = robot.data.body_pos_w[:, ee_frame_idx] - ee_quat_w = robot.data.body_quat_w[:, ee_frame_idx] + root_pos_w = robot.data.root_pos_w.torch + root_quat_w = robot.data.root_quat_w.torch + ee_pos_w = robot.data.body_pos_w.torch[:, ee_frame_idx] + ee_quat_w = robot.data.body_quat_w.torch[:, ee_frame_idx] ee_pos_b, ee_quat_b = subtract_frame_transforms(root_pos_w, root_quat_w, ee_pos_w, ee_quat_w) root_pose_w = torch.cat([root_pos_w, root_quat_w], dim=-1) ee_pose_w = torch.cat([ee_pos_w, ee_quat_w], dim=-1) ee_pose_b = torch.cat([ee_pos_b, ee_quat_b], dim=-1) # Compute the current velocity of the end-effector - ee_vel_w = robot.data.body_vel_w[:, ee_frame_idx, :] # Extract end-effector velocity in the world frame - root_vel_w = robot.data.root_vel_w # Extract root velocity in the world frame + ee_vel_w = robot.data.body_vel_w.torch[:, ee_frame_idx, :] # Extract end-effector velocity in the world frame + root_vel_w = robot.data.root_vel_w.torch # Extract root velocity in the world frame relative_vel_w = ee_vel_w - root_vel_w # Compute the relative velocity in the world frame - ee_lin_vel_b = quat_apply_inverse(robot.data.root_quat_w, relative_vel_w[:, 0:3]) # From world to root frame - ee_ang_vel_b = quat_apply_inverse(robot.data.root_quat_w, relative_vel_w[:, 3:6]) + ee_lin_vel_b = quat_apply_inverse(robot.data.root_quat_w.torch, relative_vel_w[:, 0:3]) # From world to root frame + ee_ang_vel_b = quat_apply_inverse(robot.data.root_quat_w.torch, relative_vel_w[:, 3:6]) ee_vel_b = torch.cat([ee_lin_vel_b, ee_ang_vel_b], dim=-1) # Calculate the contact force @@ -346,14 +351,14 @@ def update_states( contact_forces.update(sim_dt) # update contact sensor # Calculate the contact force by averaging over last four time steps (i.e., to smoothen) and # taking the max of three surfaces as only one should be the contact of interest - ee_force_w, _ = torch.max(torch.mean(contact_forces.data.net_forces_w_history, dim=1), dim=1) + ee_force_w, _ = torch.max(torch.mean(contact_forces.data.net_normal_forces_w_history, dim=1), dim=1) # This is a simplification, only for the sake of testing. ee_force_b = ee_force_w # Get joint positions and velocities - joint_pos = robot.data.joint_pos[:, arm_joint_ids] - joint_vel = robot.data.joint_vel[:, arm_joint_ids] + joint_pos = robot.data.joint_pos.torch[:, arm_joint_ids] + joint_vel = robot.data.joint_vel.torch[:, arm_joint_ids] return ( jacobian_b, diff --git a/scripts/tutorials/06_deploy/anymal_c_env.py b/scripts/tutorials/06_deploy/anymal_c_env.py new file mode 100644 index 00000000..895bd8da --- /dev/null +++ b/scripts/tutorials/06_deploy/anymal_c_env.py @@ -0,0 +1,215 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# ruff: noqa: I001 + +from __future__ import annotations + +import gymnasium as gym +import torch +import warp as wp + +import isaaclab.sim as sim_utils +from isaaclab import cloner +from isaaclab.assets import Articulation +from isaaclab.envs import DirectRLEnv +from isaaclab.sensors import ContactSensor, RayCaster + +from .anymal_c_env_cfg import AnymalCFlatEnvCfg, AnymalCRoughEnvCfg +from leapp import annotate # isort: skip + + +class AnymalCEnv(DirectRLEnv): + cfg: AnymalCFlatEnvCfg | AnymalCRoughEnvCfg + + def __init__(self, cfg: AnymalCFlatEnvCfg | AnymalCRoughEnvCfg, render_mode: str | None = None, **kwargs): + super().__init__(cfg, render_mode, **kwargs) + + 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 + ) + + self._commands = torch.zeros(self.num_envs, 3, device=self.device) + + self._episode_sums = { + key: torch.zeros(self.num_envs, dtype=torch.float, device=self.device) + for key in [ + "track_lin_vel_xy_exp", + "track_ang_vel_z_exp", + "lin_vel_z_l2", + "ang_vel_xy_l2", + "dof_torques_l2", + "dof_acc_l2", + "action_rate_l2", + "feet_air_time", + "undesired_contacts", + "flat_orientation_l2", + ] + } + self._base_id, _ = self._contact_sensor.find_sensors("base") + self._feet_ids, _ = self._contact_sensor.find_sensors(".*FOOT") + self._undesired_contact_body_ids, _ = self._contact_sensor.find_sensors(".*THIGH") + + def _setup_scene(self): + self._robot = Articulation(self.cfg.robot) + self.scene.articulations["robot"] = self._robot + self._contact_sensor = ContactSensor(self.cfg.contact_sensor) + self.scene.sensors["contact_sensor"] = self._contact_sensor + if isinstance(self.cfg, AnymalCRoughEnvCfg): + self._height_scanner = RayCaster(self.cfg.height_scanner) + self.scene.sensors["height_scanner"] = self._height_scanner + self.cfg.terrain.num_envs = self.scene.cfg.num_envs + self.cfg.terrain.env_spacing = self.scene.cfg.env_spacing + self._terrain = self.cfg.terrain.class_type(self.cfg.terrain) + src, dest = "/World/envs/env_0", "/World/envs/env_{}" + pos = cloner.grid_transforms(self.scene.num_envs, self.scene.cfg.env_spacing, device=self.device)[0] + global_paths = (self.cfg.terrain.prim_path,) + plan = cloner.clone_plan_from_env_0(src, dest, self.scene.num_envs, self.device, pos, global_paths=global_paths) + cloner.replicate(plan, stage=self.scene.stage) + # PhysX replication requires explicit collision filtering between environments. + if "physx" in self.scene.physics_backend: + self.scene.filter_collisions(global_prim_paths=[self.cfg.terrain.prim_path]) + light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)) + light_cfg.func("/World/Light", light_cfg) + + def _pre_physics_step(self, actions: torch.Tensor): + self._actions = actions.clone() + self._processed_actions = self.cfg.action_scale * self._actions + self._robot.data.default_joint_pos.torch + # start LEAPP annotations for outputs + annotate.update_state(self.spec.id, {"previous_actions": actions}) + annotate.output_tensors(self.spec.id, {"processed_actions": self._processed_actions}, export_with="onnx-dynamo") + # end LEAPP annotations for outputs + + def _apply_action(self): + self._robot.set_joint_position_target_index(target=self._processed_actions) + + def _get_observations(self) -> dict: + self._previous_actions = self._actions.clone() + height_data = None + if isinstance(self.cfg, AnymalCRoughEnvCfg): + height_data = ( + self._height_scanner.data.pos_w.torch[:, 2].unsqueeze(1) + - self._height_scanner.data.ray_hits_w.torch[..., 2] + - 0.5 + ).clip(-1.0, 1.0) + # start LEAPP annotations for inputs + root_lin_vel_b = annotate.input_tensors(self.spec.id, {"root_lin_vel_b": self._robot.data.root_lin_vel_b.torch}) + root_ang_vel_b = annotate.input_tensors(self.spec.id, {"root_ang_vel_b": self._robot.data.root_ang_vel_b.torch}) + projected_gravity_b = annotate.input_tensors( + self.spec.id, {"projected_gravity_b": self._robot.data.projected_gravity_b.torch} + ) + commands = annotate.input_tensors(self.spec.id, {"commands": self._commands}) + joint_pos = annotate.input_tensors(self.spec.id, {"joint_pos": self._robot.data.joint_pos.torch}) + default_joint_pos = annotate.input_tensors( + self.spec.id, {"default_joint_pos": self._robot.data.default_joint_pos.torch} + ) + joint_vel = annotate.input_tensors(self.spec.id, {"joint_vel": self._robot.data.joint_vel.torch}) + if height_data is not None: + height_data = annotate.input_tensors(self.spec.id, {"height_data": height_data}) + previous_actions = annotate.state_tensors(self.spec.id, {"previous_actions": self._actions}) + # end LEAPP annotations for inputs + + obs = torch.cat( + [ + tensor + for tensor in ( + root_lin_vel_b, + root_ang_vel_b, + projected_gravity_b, + commands, + joint_pos - default_joint_pos, + joint_vel, + height_data, + previous_actions, + ) + if tensor is not None + ], + dim=-1, + ) + observations = {"policy": obs} + return observations + + def _get_rewards(self) -> torch.Tensor: + lin_vel_error = torch.sum( + torch.square(self._commands[:, :2] - self._robot.data.root_lin_vel_b.torch[:, :2]), dim=1 + ) + lin_vel_error_mapped = torch.exp(-lin_vel_error / 0.25) + yaw_rate_error = torch.square(self._commands[:, 2] - self._robot.data.root_ang_vel_b.torch[:, 2]) + yaw_rate_error_mapped = torch.exp(-yaw_rate_error / 0.25) + z_vel_error = torch.square(self._robot.data.root_lin_vel_b.torch[:, 2]) + ang_vel_error = torch.sum(torch.square(self._robot.data.root_ang_vel_b.torch[:, :2]), dim=1) + joint_torques = torch.sum(torch.square(self._robot.actuators.applied_effort.torch), dim=1) + joint_accel = torch.sum(torch.square(self._robot.data.joint_acc.torch), dim=1) + action_rate = torch.sum(torch.square(self._actions - self._previous_actions), dim=1) + first_contact = self._contact_sensor.compute_first_contact(self.step_dt).torch[:, self._feet_ids] + last_air_time = self._contact_sensor.data.last_air_time.torch[:, self._feet_ids] + air_time = torch.sum((last_air_time - 0.5) * first_contact, dim=1) * ( + torch.linalg.norm(self._commands[:, :2], dim=1) > 0.1 + ) + net_contact_forces = self._contact_sensor.data.net_normal_forces_w_history.torch + is_contact = ( + torch.max(torch.linalg.norm(net_contact_forces[:, :, self._undesired_contact_body_ids], dim=-1), dim=1)[0] + > 1.0 + ) + contacts = torch.sum(is_contact, dim=1) + flat_orientation = torch.sum(torch.square(self._robot.data.projected_gravity_b.torch[:, :2]), dim=1) + + rewards = { + "track_lin_vel_xy_exp": lin_vel_error_mapped * self.cfg.lin_vel_reward_scale * self.step_dt, + "track_ang_vel_z_exp": yaw_rate_error_mapped * self.cfg.yaw_rate_reward_scale * self.step_dt, + "lin_vel_z_l2": z_vel_error * self.cfg.z_vel_reward_scale * self.step_dt, + "ang_vel_xy_l2": ang_vel_error * self.cfg.ang_vel_reward_scale * self.step_dt, + "dof_torques_l2": joint_torques * self.cfg.joint_torque_reward_scale * self.step_dt, + "dof_acc_l2": joint_accel * self.cfg.joint_accel_reward_scale * self.step_dt, + "action_rate_l2": action_rate * self.cfg.action_rate_reward_scale * self.step_dt, + "feet_air_time": air_time * self.cfg.feet_air_time_reward_scale * self.step_dt, + "undesired_contacts": contacts * self.cfg.undesired_contact_reward_scale * self.step_dt, + "flat_orientation_l2": flat_orientation * self.cfg.flat_orientation_reward_scale * self.step_dt, + } + reward = torch.sum(torch.stack(list(rewards.values())), dim=0) + for key, value in rewards.items(): + self._episode_sums[key] += value + return reward + + def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]: + time_out = self.episode_length_buf >= self.max_episode_length - 1 + net_contact_forces = self._contact_sensor.data.net_normal_forces_w_history.torch + died = torch.any( + torch.max(torch.linalg.norm(net_contact_forces[:, :, self._base_id], dim=-1), dim=1)[0] > 1.0, dim=1 + ) + return died, time_out + + def _reset_idx(self, env_ids: torch.Tensor | None): + if env_ids is None or len(env_ids) == self.num_envs: + env_ids = wp.to_torch(self._robot._ALL_INDICES) + assert env_ids is not None + self._robot.reset(env_ids) + super()._reset_idx(env_ids) + if len(env_ids) == self.num_envs: + self.episode_length_buf[:] = torch.randint_like(self.episode_length_buf, high=int(self.max_episode_length)) + self._actions[env_ids] = 0.0 + self._previous_actions[env_ids] = 0.0 + self._commands[env_ids] = torch.zeros_like(self._commands[env_ids]).uniform_(-1.0, 1.0) + joint_pos = self._robot.data.default_joint_pos.torch[env_ids] + joint_vel = self._robot.data.default_joint_vel.torch[env_ids] + default_root_pose = self._robot.data.default_root_pose.torch[env_ids] + default_root_vel = self._robot.data.default_root_vel.torch[env_ids] + default_root_pose[:, :3] += self._terrain.env_origins[env_ids] + self._robot.write_root_pose_to_sim_index(root_pose=default_root_pose, env_ids=env_ids) + self._robot.write_root_velocity_to_sim_index(root_velocity=default_root_vel, env_ids=env_ids) + self._robot.write_joint_position_to_sim_index(position=joint_pos, env_ids=env_ids) + self._robot.write_joint_velocity_to_sim_index(velocity=joint_vel, env_ids=env_ids) + extras = dict() + for key in self._episode_sums.keys(): + episodic_sum_avg = torch.mean(self._episode_sums[key][env_ids]) + extras["Episode_Reward/" + key] = episodic_sum_avg / self.max_episode_length_s + self._episode_sums[key][env_ids] = 0.0 + self.extras["log"] = dict() + self.extras["log"].update(extras) + extras = dict() + extras["Episode_Termination/base_contact"] = torch.count_nonzero(self.reset_terminated[env_ids]).item() + extras["Episode_Termination/time_out"] = torch.count_nonzero(self.reset_time_outs[env_ids]).item() + self.extras["log"].update(extras) diff --git a/scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py b/scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py new file mode 100644 index 00000000..9dbd9bf4 --- /dev/null +++ b/scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py @@ -0,0 +1,180 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +This script demonstrates the visualizer tiled camera panel. + +.. code-block:: bash + + # Kit visualizer tiled camera panel + uv run python scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py \ + --task Isaac-Velocity-Rough-AnymalD --num_envs 256 --viz kit + + # Newton visualizer tiled camera panel + uv run python scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py \ + --task IsaacContrib-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor --num_envs 25 --viz newton + +""" + +from __future__ import annotations + +import argparse +import contextlib +import sys + +import gymnasium as gym +import torch + +import isaaclab_tasks # noqa: F401 + +with contextlib.suppress(ImportError): + import isaaclab_tasks_experimental # noqa: F401 +from isaaclab.app import add_launcher_args, launch_simulation + +from isaaclab_tasks.utils import resolve_task_config, setup_preset_cli + +KIT_DEFAULT_TASK = "Isaac-Velocity-Rough-AnymalD" +NEWTON_DEFAULT_TASK = "IsaacContrib-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor" +SUPPORTED_TILED_VISUALIZERS = {"kit", "newton", "newton_gl", "newton_rtx"} +UNSUPPORTED_TILED_VISUALIZERS = {"rerun", "viser"} + + +def _resolve_env_regex_path(prim_path: str) -> str: + """Resolve scene config env namespace macros to the cloned-env regex.""" + return prim_path.format(ENV_REGEX_NS="/World/envs/env_.*") + + +def _requested_visualizers(args_cli: argparse.Namespace) -> list[str]: + """Return requested visualizers, defaulting to Kit for this tutorial.""" + visualizers = args_cli.visualizer or ["kit"] + visualizers = [str(visualizer).lower() for visualizer in visualizers] + + if "none" in visualizers: + raise ValueError("This demo requires a tiled-camera visualizer. Use '--viz kit' or '--viz newton_gl'.") + unsupported = sorted(set(visualizers) & UNSUPPORTED_TILED_VISUALIZERS) + if unsupported: + raise ValueError( + "The visualizer tiled camera panel is only implemented for Kit and Newton. " + f"Unsupported selection: {unsupported}." + ) + unknown = sorted(set(visualizers) - SUPPORTED_TILED_VISUALIZERS) + if unknown: + raise ValueError(f"Unknown visualizer selection for this demo: {unknown}.") + return visualizers + + +def _make_kit_visualizer_cfg(env_cfg): + """Create the Kit streaming-camera visualizer for the selected task.""" + from isaaclab_visualizers.kit import KitVisualizerCfg + + visualizer_cfg = KitVisualizerCfg() + visualizer_cfg.streaming_view = True + visualizer_cfg.streaming_envs = 36 + + ego_cam_cfg = getattr(env_cfg.scene, "ego_cam", None) + if ego_cam_cfg is not None: + visualizer_cfg.streaming_sensor_prim_path = _resolve_env_regex_path(ego_cam_cfg.prim_path) + return visualizer_cfg + + visualizer_cfg.streaming_sensor_prim_path = None + visualizer_cfg.streaming_cam_eye = (3.0, 3.0, 3.0) + visualizer_cfg.streaming_cam_target_prim_path = "/World/envs/*/Robot/base" + # Here is an alternative eye position for a top down view + # visualizer_cfg.streaming_cam_eye = (0.0, 0.0, 5.0) + return visualizer_cfg + + +def _make_newton_visualizer_cfg(env_cfg): + """Create the Newton streaming-camera visualizer for the selected task.""" + from isaaclab_visualizers.newton import NewtonGLVisualizerCfg + + visualizer_cfg = NewtonGLVisualizerCfg() + visualizer_cfg.streaming_view = True + visualizer_cfg.streaming_envs = 12 + + ego_cam_cfg = getattr(env_cfg.scene, "ego_cam", None) + if ego_cam_cfg is not None: + visualizer_cfg.streaming_sensor_prim_path = _resolve_env_regex_path(ego_cam_cfg.prim_path) + return visualizer_cfg + + # Here are other robot mounted camera options for this environment + # visualizer_cfg.streaming_sensor_prim_path = "/World/envs/env_.*/Robot/left_arm_camera_sim_view_frame/left_camera" + # visualizer_cfg.streaming_sensor_prim_path = ( + # "/World/envs/env_.*/Robot/right_arm_camera_sim_view_frame/right_camera" + # ) + visualizer_cfg.streaming_sensor_prim_path = None + visualizer_cfg.streaming_cam_eye = (3.0, 3.0, 3.0) + visualizer_cfg.streaming_cam_target_prim_path = "/World/envs/*/Robot/base" + return visualizer_cfg + + +def _configure_visualizers(env_cfg, args_cli: argparse.Namespace) -> None: + """Attach tiled camera visualizer configs to the environment simulation config.""" + visualizers = _requested_visualizers(args_cli) + args_cli.visualizer = visualizers + env_cfg.sim.visualizer_cfgs = [ + _make_kit_visualizer_cfg(env_cfg) if visualizer == "kit" else _make_newton_visualizer_cfg(env_cfg) + for visualizer in visualizers + ] + + +def _resolve_task(args_cli: argparse.Namespace) -> str: + """Resolve the task for the selected visualizer.""" + if args_cli.task is not None: + return args_cli.task + if "newton" in _requested_visualizers(args_cli): + return NEWTON_DEFAULT_TASK + return KIT_DEFAULT_TASK + + +# add argparse arguments +parser = argparse.ArgumentParser(description="Showcase the Kit/Newton visualizer tiled camera panel.") +parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") +parser.add_argument("--task", type=str, default=None, help="Name of the task.") +# append AppLauncher cli args +add_launcher_args(parser) +args_cli, hydra_args = setup_preset_cli(parser) +args_cli.task = _resolve_task(args_cli) +sys.argv = [sys.argv[0]] + hydra_args + + +def main(): + """Run a random-action environment with a tiled camera visualizer.""" + # parse configuration via Hydra (supports preset selection, e.g. presets=newton_mjwarp) + env_cfg, _ = resolve_task_config(args_cli.task, "") + _configure_visualizers(env_cfg, args_cli) + + with launch_simulation(env_cfg, args_cli): + # override with CLI arguments + env_cfg.scene.num_envs = args_cli.num_envs if args_cli.num_envs is not None else env_cfg.scene.num_envs + env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device + + # create environment + env = gym.make(args_cli.task, cfg=env_cfg) + + # print info (this is vectorized environment) + print(f"[INFO]: Gym observation space: {env.observation_space}") + print(f"[INFO]: Gym action space: {env.action_space}") + env.reset() + + # keep stepping until all visualizer windows have been closed + sim = env.unwrapped.sim + if not sim.visualizers: + print("[WARN]: No visualizers found. Exiting.") + env.close() + return + + while True: + if sim.visualizers and not any(v.is_running() and not v.is_closed for v in sim.visualizers): + break + with torch.inference_mode(): + actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1 + env.step(actions) + + env.close() + + +if __name__ == "__main__": + main() diff --git a/scripts/tutorials/07_visualizers/run_video_recording.py b/scripts/tutorials/07_visualizers/run_video_recording.py new file mode 100644 index 00000000..66caad8f --- /dev/null +++ b/scripts/tutorials/07_visualizers/run_video_recording.py @@ -0,0 +1,256 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Tutorial: recording video from visualizers and scene sensors. + +This script demonstrates three progressively richer recording configurations, +all using the Shadow Hand cube-reorientation task +(``Isaac-Reorient-Cube-Shadow-Camera-Direct``). + +Example 1 — Kit viewport (simplest) + One clip from the Kit interactive viewport, showing 4 parallel environments. + + .. code-block:: bash + + uv run python scripts/tutorials/07_visualizers/run_video_recording.py \ + --example 1 --num_envs 4 + +Example 2 — scene sensor only, headless + One clip captured directly from the scene's tiled-camera sensor. + No visualizer window opens; the sensor renders offline. + + .. code-block:: bash + + uv run python scripts/tutorials/07_visualizers/run_video_recording.py \ + --example 2 --num_envs 16 + +Example 3 — Kit viewport + Kit tiled grid + Newton viewport + scene sensor + Four independent clip streams recorded simultaneously. + + .. code-block:: bash + + uv run python scripts/tutorials/07_visualizers/run_video_recording.py \ + --example 3 --num_envs 4 + +Clips are written to ``videos/recording_tutorial/example_/`` in the working directory. +Examples 1 and 2 each demonstrate one recording source; Example 3 combines all of them. +""" + +from __future__ import annotations + +import argparse +import contextlib +import os +import sys + +import gymnasium as gym +import torch + +import isaaclab_tasks # noqa: F401 + +with contextlib.suppress(ImportError): + import isaaclab_tasks_experimental # noqa: F401 + +from isaaclab.app import add_launcher_args, launch_simulation +from isaaclab.envs.utils.video_recorder_cfg import VideoRecorderCfg + +from isaaclab_tasks.utils import resolve_task_config, setup_preset_cli + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +_VIDEO_LENGTH = 100 # env steps per clip +_NUM_STEPS = 115 # slightly more than _VIDEO_LENGTH so the clip flushes cleanly + +_TASK_SHADOW = "Isaac-Reorient-Cube-Shadow-Camera-Direct" + +# Kit viewport camera: positioned to show a 2×2 grid of Shadow Hand environments. +# env_spacing=1.0 with 4 envs → envs centered at ±0.5 in x and y. +# Cube spawns at ~(0, -0.39, 0.6) per env; wrist cylinder is the landmark at the top. +_SHADOW_EYE = (0.0, -2.2, 1.8) +_SHADOW_LOOKAT = (0.0, -0.1, 0.4) +_SHADOW_ENV_SPACING = 1.0 + +# Skip the first few steps so the RTX renderer has warmed up before recording starts. +_KIT_STEP_OFFSET = 5 + + +def _output_dir(example: int) -> str: + return os.path.join("videos", "recording_tutorial", f"example_{example}") + + +def _shadow_env_cfg(num_envs: int, env_spacing: float = _SHADOW_ENV_SPACING): + """Build a base Shadow Hand camera env cfg shared by all examples.""" + env_cfg, _ = resolve_task_config(_TASK_SHADOW, "", overrides=(*sys.argv[1:], "env.tiled_camera=rgb")) + env_cfg.tiled_camera.height = 256 + env_cfg.tiled_camera.width = 256 + env_cfg.scene.num_envs = num_envs + env_cfg.scene.env_spacing = env_spacing + return env_cfg + + +# --------------------------------------------------------------------------- +# Per-example environment config builders +# --------------------------------------------------------------------------- + + +def _build_env_cfg_example_1(num_envs: int): + """Shadow Hand + Kit viewport: one clip from the interactive viewport.""" + from isaaclab_visualizers.kit import KitVisualizerCfg + + env_cfg = _shadow_env_cfg(num_envs) + env_cfg.sim.visualizer_cfgs = [KitVisualizerCfg(eye=_SHADOW_EYE, lookat=_SHADOW_LOOKAT)] + + out = _output_dir(1) + env_cfg.video_recorders = [ + VideoRecorderCfg( + source="visualizer:kit", + output_dir=out, + output_filename_prefix="kit_viewport", + video_length=_VIDEO_LENGTH, + fps=30, + step_offset=_KIT_STEP_OFFSET, + ), + ] + return env_cfg, _TASK_SHADOW + + +def _build_env_cfg_example_2(num_envs: int): + """Shadow Hand + headless: scene tiled-camera sensor clip only.""" + env_cfg = _shadow_env_cfg(num_envs, env_spacing=2.0) + env_cfg.sim.visualizer_cfgs = [] # no interactive visualizer + + out = _output_dir(2) + env_cfg.video_recorders = [ + VideoRecorderCfg( + source="sensor:tiled_camera", + output_dir=out, + output_filename_prefix="sensor", + video_length=_VIDEO_LENGTH, + fps=30, + ), + ] + return env_cfg, _TASK_SHADOW + + +def _build_env_cfg_example_3(num_envs: int): + """Shadow Hand + Kit viewport + Kit tiled grid + Newton viewport + sensor: four simultaneous streams. + + Note: ``source='visualizer:newton'`` captures the full Newton GL window. When + ``streaming_view=True`` is set on :class:`~isaaclab_visualizers.newton.NewtonGLVisualizerCfg`, + the GL window displays the per-environment camera panel, so this effectively records + a Newton streaming view without a separate ``render_tiled_rgb_array()`` call. + """ + from isaaclab_visualizers.kit import KitVisualizerCfg + from isaaclab_visualizers.newton import NewtonGLVisualizerCfg + + env_cfg = _shadow_env_cfg(num_envs) + kit_cfg = KitVisualizerCfg( + eye=_SHADOW_EYE, + lookat=_SHADOW_LOOKAT, + streaming_view=True, + streaming_envs=min(num_envs, 16), + # No streaming_sensor_prim_path/streaming_cam_target_prim_path: adopts the existing + # tiled_camera sensor automatically, so the streaming panel shows the same + # RTX-rendered views as source="sensor:tiled_camera". + ) + newton_cfg = NewtonGLVisualizerCfg( + eye=_SHADOW_EYE, + lookat=_SHADOW_LOOKAT, + window_width=1280, + window_height=720, + focal_length=25.0, + ) + env_cfg.sim.visualizer_cfgs = [kit_cfg, newton_cfg] + + out = _output_dir(3) + env_cfg.video_recorders = [ + VideoRecorderCfg( + source="visualizer:kit", + output_dir=out, + output_filename_prefix="kit_viewport", + video_length=_VIDEO_LENGTH, + fps=30, + step_offset=_KIT_STEP_OFFSET, + ), + VideoRecorderCfg( + source="visualizer:kit:streaming_view", + output_dir=out, + output_filename_prefix="tiled_kit_viewport", + video_length=_VIDEO_LENGTH, + fps=30, + step_offset=_KIT_STEP_OFFSET, + ), + VideoRecorderCfg( + source="visualizer:newton", + output_dir=out, + output_filename_prefix="newton_viewport", + video_length=_VIDEO_LENGTH, + fps=30, + ), + VideoRecorderCfg( + source="sensor:tiled_camera", + output_dir=out, + output_filename_prefix="sensor", + video_length=_VIDEO_LENGTH, + fps=30, + ), + ] + return env_cfg, _TASK_SHADOW + + +_BUILDERS = { + 1: _build_env_cfg_example_1, + 2: _build_env_cfg_example_2, + 3: _build_env_cfg_example_3, +} + +# --------------------------------------------------------------------------- +# Argument parsing +# --------------------------------------------------------------------------- +parser = argparse.ArgumentParser(description="Video recording tutorial for Isaac Lab environments.") +parser.add_argument( + "--example", type=int, default=1, choices=[1, 2, 3], help="Which recording example to run (1, 2, or 3)." +) +parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") +add_launcher_args(parser) +args_cli, hydra_args = setup_preset_cli(parser) +sys.argv = [sys.argv[0]] + hydra_args + + +def main(): + """Run the selected video recording example.""" + defaults = {1: 4, 2: 16, 3: 4} + num_envs = args_cli.num_envs if args_cli.num_envs is not None else defaults[args_cli.example] + env_cfg, task = _BUILDERS[args_cli.example](num_envs) + env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device + + # Examples 1 and 3 record from the Kit viewport via omni.replicator, which requires + # camera rendering support. Force it here for visualizer-only recording. + if args_cli.example in (1, 3): + args_cli.enable_cameras = True + + with launch_simulation(env_cfg, args_cli): + env = gym.make(task, cfg=env_cfg) + + out = _output_dir(args_cli.example) + print(f"[INFO]: Running Example {args_cli.example} — clips → {out}/") + print(f"[INFO]: Gym observation space: {env.observation_space}") + print(f"[INFO]: Gym action space: {env.action_space}") + + print("[INFO]: Setup complete.") + env.reset() + for _ in range(_NUM_STEPS): + with torch.inference_mode(): + actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1 + env.step(actions) + + env.close() + print(f"[INFO]: Done. Clips written to {out}/") + + +if __name__ == "__main__": + main() diff --git a/scripts_v2/tools/collect_demos.py b/scripts_v2/tools/collect_demos.py index d1de6599..1fa91619 100644 --- a/scripts_v2/tools/collect_demos.py +++ b/scripts_v2/tools/collect_demos.py @@ -28,7 +28,7 @@ parser.add_argument("--dataset_file", type=str, default="./datasets/dataset.zarr", help="Output dataset path.") parser.add_argument("--num_demos", type=int, default=10, help="Number of demonstrations to record.") parser.add_argument( - "--deterministic", + "--deterministic_expert", action="store_true", default=False, help="Use the mean of the policy distribution instead of sampling.", @@ -154,7 +154,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg, agent_cfg: RslRlOnPolic expert_policy = loader(bc.experts_path[0]).to(env_cfg.sim.device) expert_policy.eval() - print(f"[Policy] {'Deterministic (mean)' if args_cli.deterministic else 'Stochastic (sampled)'} actions") + print(f"[Policy] {'Deterministic (mean)' if args_cli.deterministic_expert else 'Stochastic (sampled)'} actions") # simulate environment -- run everything in inference mode current_recorded_demo_count = 0 @@ -166,7 +166,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg, agent_cfg: RslRlOnPolic # agent stepping expert_policy_obs = expert_obs_fn(env) mean, std = expert_policy.compute_distribution(expert_policy_obs) - actions = mean if args_cli.deterministic else torch.normal(mean, std) + actions = mean if args_cli.deterministic_expert else torch.normal(mean, std) # Mask actions to zero for environments in their first step after reset since first image may not be valid first_step_mask = env.unwrapped.episode_length_buf == 0 diff --git a/scripts_v2/tools/record_grasps.py b/scripts_v2/tools/record_grasps.py index 24b9e623..306660aa 100644 --- a/scripts_v2/tools/record_grasps.py +++ b/scripts_v2/tools/record_grasps.py @@ -22,7 +22,10 @@ parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to simulate.") parser.add_argument("--task", type=str, default="OmniReset-Robotiq2f85-GraspSampling-v0", help="Name of the task.") parser.add_argument( - "--dataset_dir", type=str, default="./Datasets/OmniReset/", help="Root Datasets/OmniReset/ directory." + "--dataset_dir", + type=str, + default="./Datasets/OmniReset_isaaclab3/", + help="Root Datasets/OmniReset_isaaclab3/ directory.", ) parser.add_argument("--num_grasps", type=int, default=500, help="Number of grasp candidates to evaluate.") diff --git a/scripts_v2/tools/record_partial_assemblies.py b/scripts_v2/tools/record_partial_assemblies.py index 66619341..52b7614b 100644 --- a/scripts_v2/tools/record_partial_assemblies.py +++ b/scripts_v2/tools/record_partial_assemblies.py @@ -22,7 +22,10 @@ parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to simulate.") parser.add_argument("--task", type=str, default="UW-FBLeg-PartialAssemblies-v0", help="Name of the task.") parser.add_argument( - "--dataset_dir", type=str, default="./Datasets/OmniReset/", help="Root Datasets/OmniReset/ directory." + "--dataset_dir", + type=str, + default="./Datasets/OmniReset_isaaclab3/", + help="Root Datasets/OmniReset_isaaclab3/ directory.", ) parser.add_argument( "--num_trajectories", type=int, default=1, help="Number of physics trajectories to run for pose discovery." @@ -196,7 +199,8 @@ def _save_poses_to_dataset(pose_batches: list, dataset_dir: str, pair_name: str) output_dir = os.path.join(dataset_dir, "Resets", pair_name) os.makedirs(output_dir, exist_ok=True) output_file = os.path.join(output_dir, "partial_assemblies.pt") - torch.save(all_poses, output_file) + # Stamp the quaternion convention; loaders refuse files without it. + torch.save({**all_poses, "quat_convention": "xyzw"}, output_file) print(f"Saved {len(all_poses['relative_position'])} poses to {output_file}") diff --git a/scripts_v2/tools/record_reset_states.py b/scripts_v2/tools/record_reset_states.py index c656bba1..51a0108f 100644 --- a/scripts_v2/tools/record_reset_states.py +++ b/scripts_v2/tools/record_reset_states.py @@ -24,7 +24,10 @@ "--task", type=str, default="OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEAnywhere-v0", help="Name of the task." ) parser.add_argument( - "--dataset_dir", type=str, default="./Datasets/OmniReset/", help="Root Datasets/OmniReset/ directory." + "--dataset_dir", + type=str, + default="./Datasets/OmniReset_isaaclab3/", + help="Root Datasets/OmniReset_isaaclab3/ directory.", ) parser.add_argument( "--reset_type", diff --git a/scripts_v2/tools/sim2real/align_cameras.py b/scripts_v2/tools/sim2real/align_cameras.py index f2ddd8fa..946cf845 100644 --- a/scripts_v2/tools/sim2real/align_cameras.py +++ b/scripts_v2/tools/sim2real/align_cameras.py @@ -16,14 +16,14 @@ Usage (front camera example): python scripts_v2/tools/sim2real/align_cameras.py \ - --enable_cameras \ + --visualizer none \ --camera front_camera \ --real_image /path/to/real_front.png \ --joint_angles -12.0 -80.0 63.0 -30.6 -97.9 174.3 Usage (wrist camera example): python scripts_v2/tools/sim2real/align_cameras.py \ - --enable_cameras \ + --visualizer none \ --camera wrist_camera \ --real_image /path/to/real_wrist.png \ --joint_angles -12.0 -80.0 63.0 -30.6 -97.9 174.3 @@ -289,7 +289,7 @@ def _print_params(self): print("--- Paste into data_collection_rgb_cfg.py ---") print("--- (same values for BOTH TiledCameraCfg.OffsetCfg AND BaseRGBEventCfg) ---") print(f" pos=({self.pos[0]:.7f}, {self.pos[1]:.7f}, {self.pos[2]:.7f}),") - print(f" rot=({self.rot[0]:.8f}, {self.rot[1]:.8f}, {self.rot[2]:.8f}, {self.rot[3]:.8f}),") + print(f" rot=({self.rot[1]:.8f}, {self.rot[2]:.8f}, {self.rot[3]:.8f}, {self.rot[0]:.8f}), # (x, y, z, w)") print(f" focal_length={fl:.2f}") print("=" * 60 + "\n") diff --git a/scripts_v2/tools/sim2real/collect_fk_pairs.py b/scripts_v2/tools/sim2real/collect_fk_pairs.py index a7f8dcb7..99da6075 100644 --- a/scripts_v2/tools/sim2real/collect_fk_pairs.py +++ b/scripts_v2/tools/sim2real/collect_fk_pairs.py @@ -119,12 +119,12 @@ def main(): obs, _, _, _, _ = env.step(zero_action) # Read all envs at once - joint_pos = robot.data.joint_pos[:, :6].cpu().numpy() # (N, 6) - ee_pos_w = robot.data.body_link_pos_w[:, ee_idx] # (N, 3) - ee_quat_w = robot.data.body_link_quat_w[:, ee_idx] # (N, 4) + joint_pos = robot.data.joint_pos.torch[:, :6].cpu().numpy() # (N, 6) + ee_pos_w = robot.data.body_link_pos_w.torch[:, ee_idx] # (N, 3) + ee_quat_w = robot.data.body_link_quat_w.torch[:, ee_idx] # (N, 4) ee_pos_b, ee_quat_b = math_utils.subtract_frame_transforms( - robot.data.root_pos_w, - robot.data.root_quat_w, + robot.data.root_pos_w.torch, + robot.data.root_quat_w.torch, ee_pos_w, ee_quat_w, ) @@ -132,10 +132,11 @@ def main(): all_joint_pos.append(joint_pos) all_ee_pos.append(ee_pos_b.cpu().numpy()) - all_ee_quat.append(ee_quat_b.cpu().numpy()) + # diffusion_policy's FK code (test_fk_comparison.py) uses (w, x, y, z) + all_ee_quat.append(math_utils.convert_quat(ee_quat_b, to="wxyz").cpu().numpy()) all_ee_aa.append(ee_aa_b.cpu().numpy()) - print(f" Reset {r+1}/{args_cli.num_resets}: collected {len(joint_pos)} pairs") + print(f" Reset {r + 1}/{args_cli.num_resets}: collected {len(joint_pos)} pairs") all_joint_pos = np.concatenate(all_joint_pos, axis=0) all_ee_pos = np.concatenate(all_ee_pos, axis=0) diff --git a/scripts_v2/tools/sim2real/eval_robustness.py b/scripts_v2/tools/sim2real/eval_robustness.py index 4eec03da..188904fb 100644 --- a/scripts_v2/tools/sim2real/eval_robustness.py +++ b/scripts_v2/tools/sim2real/eval_robustness.py @@ -113,10 +113,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen raise ValueError(f"Unsupported runner class: {agent_cfg.class_name}") runner.load(resume_path) policies.append(runner.get_inference_policy(device=env.unwrapped.device)) - try: - policy_nns.append(runner.alg.policy) - except AttributeError: - policy_nns.append(runner.alg.actor_critic) + policy_nns.append(runner.alg.actor) print(f"\n{'=' * 60}") print(f"Running {num_policies} policies across {num_envs} envs") diff --git a/scripts_v2/tools/sim2real/plot_sysid_fit.py b/scripts_v2/tools/sim2real/plot_sysid_fit.py index 8605fc8b..e1f0a713 100644 --- a/scripts_v2/tools/sim2real/plot_sysid_fit.py +++ b/scripts_v2/tools/sim2real/plot_sysid_fit.py @@ -41,7 +41,7 @@ from isaaclab.actuators import DelayedPDActuatorCfg from isaaclab.assets import Articulation -from isaaclab.utils.math import subtract_frame_transforms +from isaaclab.utils.math import convert_quat, subtract_frame_transforms from uwlab_assets.robots.ur5e_robotiq_gripper.kinematics import ARM_JOINT_NAMES, EE_BODY_NAME, NUM_ARM_JOINTS @@ -111,8 +111,8 @@ def closed_loop_replay( action_dim = unwrapped.action_manager.total_action_dim W = wp_step_indices.shape[0] - default_joint_pos = robot.data.default_joint_pos.clone() - default_joint_vel = robot.data.default_joint_vel.clone() + default_joint_pos = robot.data.default_joint_pos.torch.clone() + default_joint_vel = robot.data.default_joint_vel.torch.clone() default_joint_pos[:, arm_joint_ids] = initial_joint_pos.unsqueeze(0) default_joint_vel[:] = 0.0 env.reset() @@ -125,10 +125,10 @@ def closed_loop_replay( while wp_idx + 1 < W and t >= wp_step_indices[wp_idx + 1]: wp_idx += 1 - ee_pos_w = robot.data.body_pos_w[:, ee_frame_idx] - ee_quat_w = robot.data.body_quat_w[:, ee_frame_idx] + ee_pos_w = robot.data.body_pos_w.torch[:, ee_frame_idx] + ee_quat_w = robot.data.body_quat_w.torch[:, ee_frame_idx] ee_pos_b, ee_quat_b = subtract_frame_transforms( - robot.data.root_pos_w, robot.data.root_quat_w, ee_pos_w, ee_quat_w + robot.data.root_pos_w.torch, robot.data.root_quat_w.torch, ee_pos_w, ee_quat_w ) target_pos = wp_target_pos[wp_idx].unsqueeze(0) target_quat = wp_target_quat[wp_idx].unsqueeze(0) @@ -137,14 +137,14 @@ def closed_loop_replay( action = torch.cat([action_arm, torch.zeros(1, action_dim - 6, device=device)], dim=-1) env.step(action) - joint_pos = robot.data.joint_pos[:, arm_joint_ids] - joint_vel = robot.data.joint_vel[:, arm_joint_ids] + joint_pos = robot.data.joint_pos.torch[:, arm_joint_ids] + joint_vel = robot.data.joint_vel.torch[:, arm_joint_ids] sim_positions.append(joint_pos[0].cpu().numpy().copy()) sim_velocities.append(joint_vel[0].cpu().numpy().copy()) sim_ee_positions.append(ee_pos_b[0].cpu().numpy().copy()) if (t + 1) % max(1, T_steps // 20) == 0: - print(f" step {t+1}/{T_steps} ({100*(t+1)/T_steps:.0f}%)") + print(f" step {t + 1}/{T_steps} ({100 * (t + 1) / T_steps:.0f}%)") return { "joint_positions": np.array(sim_positions), @@ -249,14 +249,15 @@ def main(): initial_joint_pos = real_data["initial_joint_pos"] wp_step_indices = real_data["waypoint_step_indices"] wp_target_pos = real_data["waypoint_target_pos"] - wp_target_quat = real_data["waypoint_target_quat"] + # the real-robot collector (diffusion_policy) records (w, x, y, z) + wp_target_quat = convert_quat(real_data["waypoint_target_quat"], to="xyzw") dt = real_data["dt"] T_steps = real_joint_pos.shape[0] if args.max_steps is not None: T_steps = min(T_steps, args.max_steps) - print(f" {T_steps} steps ({T_steps*dt:.2f}s), dt={dt*1000:.1f}ms") + print(f" {T_steps} steps ({T_steps * dt:.2f}s), dt={dt * 1000:.1f}ms") # Move to GPU real_joint_pos_np = real_joint_pos[:T_steps].numpy() @@ -322,7 +323,7 @@ def main(): ee_frame_idx, sim_dt, T_steps, - headless=args_cli.headless, + headless=not app_launcher.has_gui(), ) sim_joints = result["joint_positions"] @@ -330,7 +331,7 @@ def main(): # Compute per-joint RMSE error_deg = np.degrees(sim_joints - real_joints) - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print("Per-joint RMSE (deg)") print("=" * 60) for j in range(NUM_ARM_JOINTS): diff --git a/scripts_v2/tools/sim2real/sysid_ur5e_osc.py b/scripts_v2/tools/sim2real/sysid_ur5e_osc.py index a6e6813f..a3cac5ec 100644 --- a/scripts_v2/tools/sim2real/sysid_ur5e_osc.py +++ b/scripts_v2/tools/sim2real/sysid_ur5e_osc.py @@ -53,7 +53,7 @@ from isaaclab.actuators import DelayedPDActuatorCfg from isaaclab.assets import Articulation -from isaaclab.utils.math import subtract_frame_transforms +from isaaclab.utils.math import convert_quat, subtract_frame_transforms from uwlab_assets.robots.ur5e_robotiq_gripper.kinematics import ARM_JOINT_NAMES, EE_BODY_NAME, NUM_ARM_JOINTS @@ -175,7 +175,8 @@ def main(): initial_joint_pos = real_data["initial_joint_pos"] wp_step_indices = real_data["waypoint_step_indices"] wp_target_pos = real_data["waypoint_target_pos"] - wp_target_quat = real_data["waypoint_target_quat"] + # the real-robot collector (diffusion_policy) records (w, x, y, z) + wp_target_quat = convert_quat(real_data["waypoint_target_quat"], to="xyzw") dt = real_data["dt"] T_steps = real_joint_pos.shape[0] @@ -183,7 +184,7 @@ def main(): T_steps = min(T_steps, args.max_steps) W = wp_step_indices.shape[0] - print(f" {T_steps} steps ({T_steps*dt:.2f}s), {W} waypoints, dt={dt*1000:.1f}ms") + print(f" {T_steps} steps ({T_steps * dt:.2f}s), {W} waypoints, dt={dt * 1000:.1f}ms") # Move to GPU real_joint_pos = real_joint_pos[:T_steps].to(device_str).float() @@ -234,8 +235,8 @@ def main(): sim_dt = env_cfg.sim.dt action_dim = unwrapped.action_manager.total_action_dim # 7 (arm 6 + gripper 1) - default_joint_pos = robot.data.default_joint_pos.clone() - default_joint_vel = robot.data.default_joint_vel.clone() + default_joint_pos = robot.data.default_joint_pos.torch.clone() + default_joint_vel = robot.data.default_joint_vel.torch.clone() default_joint_pos[:, arm_joint_ids] = initial_joint_pos_dev.unsqueeze(0).expand(N, -1) default_joint_vel[:] = 0.0 @@ -275,10 +276,10 @@ def main(): while wp_idx + 1 < W and t >= wp_step_indices[wp_idx + 1]: wp_idx += 1 - ee_pos_w = robot.data.body_pos_w[:, ee_frame_idx] - ee_quat_w = robot.data.body_quat_w[:, ee_frame_idx] + ee_pos_w = robot.data.body_pos_w.torch[:, ee_frame_idx] + ee_quat_w = robot.data.body_quat_w.torch[:, ee_frame_idx] ee_pos_b, ee_quat_b = subtract_frame_transforms( - robot.data.root_pos_w, robot.data.root_quat_w, ee_pos_w, ee_quat_w + robot.data.root_pos_w.torch, robot.data.root_quat_w.torch, ee_pos_w, ee_quat_w ) target_pos = wp_target_pos[wp_idx].unsqueeze(0).expand(N, -1) target_quat = wp_target_quat[wp_idx].unsqueeze(0).expand(N, -1) @@ -287,7 +288,7 @@ def main(): action = torch.cat([action_arm, torch.zeros(N, action_dim - 6, device=device)], dim=-1) env.step(action) - joint_pos = robot.data.joint_pos[:, arm_joint_ids] + joint_pos = robot.data.joint_pos.torch[:, arm_joint_ids] scores += torch.sum((joint_pos - real_joint_pos[t].unsqueeze(0)) ** 2, dim=1) scores = scores / T_steps @@ -309,7 +310,7 @@ def main(): best_delay = round(float(best_params_ever[24])) rmse_deg = np.degrees(np.sqrt(best_score_ever)) print( - f"[{iteration+1:3d}/{args.max_iter}] " + f"[{iteration + 1:3d}/{args.max_iter}] " f"min={min_score:.6f} mean={mean_score:.6f} best={best_score_ever:.6f} " f"({rmse_deg:.3f}\u00b0 delay={best_delay}) {iter_time:.1f}s" ) @@ -323,14 +324,14 @@ def main(): "bounds": bounds, "args": vars(args), } - ckpt_path = os.path.join(output_dir, f"checkpoint_{iteration+1:04d}.pt") + ckpt_path = os.path.join(output_dir, f"checkpoint_{iteration + 1:04d}.pt") torch.save(ckpt, ckpt_path) print(f" -> {ckpt_path}") # Final results - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(f"DONE RMSE: {np.degrees(np.sqrt(best_score_ever)):.4f}\u00b0") - print(f"{'='*60}") + print(f"{'=' * 60}") arm = best_params_ever[:6] sfric = best_params_ever[6:12] @@ -342,7 +343,7 @@ def main(): print(f"\n {'Joint':<25s} {'Arm':>8s} {'SFric':>8s} {'DRat':>8s} {'DFric':>8s} {'VFric':>8s}") for i, name in enumerate(ARM_JOINT_NAMES): print(f" {name:<25s} {arm[i]:8.4f} {sfric[i]:8.4f} {dratio[i]:8.4f} {dfric[i]:8.4f} {vfric[i]:8.4f}") - print(f"\n Motor delay: {delay} steps ({delay*sim_dt*1000:.0f}ms at {1/sim_dt:.0f}Hz)") + print(f"\n Motor delay: {delay} steps ({delay * sim_dt * 1000:.0f}ms at {1 / sim_dt:.0f}Hz)") final = { "best_params": best_params_ever, diff --git a/scripts_v2/tools/visualize_reset_states.py b/scripts_v2/tools/visualize_reset_states.py index db2eb403..25f505d3 100644 --- a/scripts_v2/tools/visualize_reset_states.py +++ b/scripts_v2/tools/visualize_reset_states.py @@ -21,7 +21,7 @@ parser.add_argument( "--dataset_dir", type=str, - default="./Datasets/OmniReset", + default="./Datasets/OmniReset_isaaclab3", help="Base dataset directory (contains Resets// subdirectories).", ) parser.add_argument( @@ -35,8 +35,9 @@ AppLauncher.add_app_launcher_args(parser) args_cli, remaining_args = parser.parse_known_args() -# launch omniverse app -app_launcher = AppLauncher(headless=args_cli.headless) +# launch omniverse app -- pass the whole namespace: forwarding only `headless` parses +# the other AppLauncher flags (--viz, --device, --enable_cameras, ...) and drops them. +app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app """Rest everything else.""" diff --git a/source/uwlab/config/extension.toml b/source/uwlab/config/extension.toml index 8d0e7718..79ad16c1 100644 --- a/source/uwlab/config/extension.toml +++ b/source/uwlab/config/extension.toml @@ -1,7 +1,7 @@ [package] # Semantic Versioning is used: https://semver.org/ -version = "0.8.6" +version = "0.9.0" # Description title = "UW Lab framework for Robot Learning" diff --git a/source/uwlab/docs/CHANGELOG.rst b/source/uwlab/docs/CHANGELOG.rst index 0e2e8243..753c7717 100644 --- a/source/uwlab/docs/CHANGELOG.rst +++ b/source/uwlab/docs/CHANGELOG.rst @@ -1,6 +1,19 @@ Changelog --------- +0.9.0 (2026-09-28) +~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Migrated to Isaac Lab 3.0 Early Access / Isaac Sim 6.1: scalar-last quaternions, + Warp-backed asset data (``.torch``), ``root_view`` / ``data`` accessors, and + ``isaaclab.sim.utils`` in place of deprecated Isaac Sim helpers. +* Updated mesh conversion to enable Isaac Sim extensions through the Isaac Lab API. + Launch the simulator before invoking conversion helpers. + + 0.8.6 (2025-10-09) ~~~~~~~~~~~~~~~~~~ diff --git a/source/uwlab/uwlab/assets/articulation/articulation.py b/source/uwlab/uwlab/assets/articulation/articulation.py index 5361e575..060b8bc4 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation.py +++ b/source/uwlab/uwlab/assets/articulation/articulation.py @@ -13,9 +13,9 @@ from prettytable import PrettyTable from typing import TYPE_CHECKING +import isaaclab.sim.utils.stage as stage_utils import isaaclab.utils.math as math_utils import isaaclab.utils.string as string_utils -import isaacsim.core.utils.stage as stage_utils import omni.log from isaaclab.actuators import ActuatorBase, ActuatorBaseCfg, ImplicitActuator from isaaclab.utils.types import ArticulationActions @@ -1433,7 +1433,7 @@ def _validate_cfg(self): for idx in violated_indices: joint_name = self.data.joint_names[idx] joint_limits = joint_pos_limits[idx] - joint_pos = self.data.default_joint_pos[0, idx] + joint_pos = self.data.default_joint_pos.torch[0, idx] # add to message msg += f"\t- '{joint_name}': {joint_pos:.3f} not in [{joint_limits[0]:.3f}, {joint_limits[1]:.3f}]\n" raise ValueError(msg) @@ -1448,7 +1448,7 @@ def _validate_cfg(self): for idx in violated_indices: joint_name = self.data.joint_names[idx] joint_limits = [-joint_max_vel[idx], joint_max_vel[idx]] - joint_vel = self.data.default_joint_vel[0, idx] + joint_vel = self.data.default_joint_vel.torch[0, idx] # add to message msg += f"\t- '{joint_name}': {joint_vel:.3f} not in [{joint_limits[0]:.3f}, {joint_limits[1]:.3f}]\n" raise ValueError(msg) diff --git a/source/uwlab/uwlab/devices/teleop.py b/source/uwlab/uwlab/devices/teleop.py index 214be523..535e4388 100644 --- a/source/uwlab/uwlab/devices/teleop.py +++ b/source/uwlab/uwlab/devices/teleop.py @@ -43,10 +43,10 @@ class TeleopState: def update_ref(self, robot: Articulation): ref_body_id = self.pose_reference_body.body_ids ref_pos_b, ref_quat_b = math_utils.subtract_frame_transforms( - robot.data.root_pos_w, - robot.data.root_quat_w, - robot.data.body_link_pos_w[:, ref_body_id, :].view(-1, 3), - robot.data.body_link_quat_w[:, ref_body_id, :].view(-1, 4), + robot.data.root_pos_w.torch, + robot.data.root_quat_w.torch, + robot.data.body_link_pos_w.torch[:, ref_body_id, :].view(-1, 3), + robot.data.body_link_quat_w.torch[:, ref_body_id, :].view(-1, 4), ) self.ref_pos_b = ref_pos_b.repeat_interleave(self.num_command_body, dim=0) self.ref_quat_b = ref_quat_b.repeat_interleave(self.num_command_body, dim=0) @@ -54,10 +54,10 @@ def update_ref(self, robot: Articulation): def update_attach(self, robot: Articulation): attach_body_id = self.attach_body.body_ids self.attach_pos_b, self.attach_quat_b = math_utils.subtract_frame_transforms( - robot.data.root_pos_w, - robot.data.root_quat_w, - robot.data.body_link_pos_w[:, attach_body_id, :].view(-1, 3), - robot.data.body_link_quat_w[:, attach_body_id, :].view(-1, 4), + robot.data.root_pos_w.torch, + robot.data.root_quat_w.torch, + robot.data.body_link_pos_w.torch[:, attach_body_id, :].view(-1, 3), + robot.data.body_link_quat_w.torch[:, attach_body_id, :].view(-1, 4), ) self.attach_pos_b = self.attach_pos_b.repeat_interleave(self.num_command_body, dim=0) self.attach_quat_b = self.attach_quat_b.repeat_interleave(self.num_command_body, dim=0) @@ -66,8 +66,8 @@ def combine_frame_on_root( self, robot: Articulation, command_pos_b: torch.Tensor, command_quat_b: torch.Tensor | None ): command_pos_w, command_quat_w = math_utils.combine_frame_transforms( - robot.data.root_pos_w.repeat_interleave(self.num_command_body, dim=0), - robot.data.root_quat_w.repeat_interleave(self.num_command_body, dim=0), + robot.data.root_pos_w.torch.repeat_interleave(self.num_command_body, dim=0), + robot.data.root_quat_w.torch.repeat_interleave(self.num_command_body, dim=0), command_pos_b, command_quat_b, ) diff --git a/source/uwlab/uwlab/envs/diagnosis/diagnosis.py b/source/uwlab/uwlab/envs/diagnosis/diagnosis.py index a3008b89..6a1a5fc1 100644 --- a/source/uwlab/uwlab/envs/diagnosis/diagnosis.py +++ b/source/uwlab/uwlab/envs/diagnosis/diagnosis.py @@ -9,6 +9,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING +import warp as wp from isaaclab.assets import Articulation from isaaclab.managers import SceneEntityCfg @@ -16,6 +17,15 @@ from isaaclab.envs import ManagerBasedRLEnv +def _as_torch(value) -> torch.Tensor: + """Return a physics-view result (``wp.array`` in Isaac Lab 3.0) as a torch tensor.""" + if hasattr(value, "torch"): + return value.torch + if isinstance(value, wp.array): + return wp.to_torch(value) + return value + + def get_link_incoming_joint_force( env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, @@ -33,7 +43,7 @@ def get_link_incoming_joint_force( asset: Articulation = env.scene[asset_cfg.name] if env_ids is None: env_ids = slice(None) - force_from_child_link_to_joints = asset.root_physx_view.get_link_incoming_joint_force().to(env.device)[env_ids] + force_from_child_link_to_joints = _as_torch(asset.root_view.get_link_incoming_joint_force()).to(env.device)[env_ids] return force_from_child_link_to_joints @@ -56,7 +66,7 @@ def get_dof_projected_joint_forces( asset: Articulation = env.scene[asset_cfg.name] if env_ids is None: env_ids = slice(None) - projected_joint_forces = asset.root_physx_view.get_dof_projected_joint_forces().to(env.device)[env_ids] + projected_joint_forces = _as_torch(asset.root_view.get_dof_projected_joint_forces()).to(env.device)[env_ids] return projected_joint_forces @@ -108,7 +118,7 @@ def get_dof_position( asset: Articulation = env.scene[asset_cfg.name] if env_ids is None: env_ids = slice(None) - joint_pos = asset.data.joint_pos[env_ids] + joint_pos = asset.data.joint_pos.torch[env_ids] return joint_pos @@ -164,7 +174,7 @@ def get_joint_torque_utilization( if env_ids is None: env_ids = slice(None) applied_torque = asset.data.applied_torque[env_ids] - torque_max = asset.root_physx_view.get_dof_max_forces().to(env.device)[env_ids] + torque_max = asset.data.joint_effort_limits.torch.to(env.device)[env_ids] torque_utilization = torch.abs(applied_torque) / torque_max return torque_utilization @@ -178,7 +188,7 @@ def get_joint_velocity_utilization( if env_ids is None: env_ids = slice(None) joint_vel = asset.data.joint_vel[env_ids] - max_vel = asset.root_physx_view.get_dof_max_velocities().to(env.device)[env_ids] + max_vel = asset.data.joint_vel_limits.torch.to(env.device)[env_ids] velocity_utilization = torch.abs(joint_vel) / max_vel return velocity_utilization @@ -205,7 +215,7 @@ def get_joint_mechanical_work( asset: Articulation = env.scene[asset_cfg.name] if env_ids is None: env_ids = slice(None) - joint_pos = asset.data.joint_pos[env_ids] + joint_pos = asset.data.joint_pos.torch[env_ids] applied_torque = asset.data.applied_torque[env_ids] if "prev_joint_pos" in env.extensions: delta_joint_pos = joint_pos - env.extensions["prev_joint_pos"] @@ -241,7 +251,7 @@ def effective_torque( applied_torque = asset.data.applied_torque[env_ids] # Shape: (num_envs, num_joints) # Get projected joint forces - projected_joint_forces = asset.root_physx_view.get_dof_projected_joint_forces().to(env.device)[ + projected_joint_forces = _as_torch(asset.root_view.get_dof_projected_joint_forces()).to(env.device)[ env_ids ] # Shape: (num_envs, num_joints) @@ -275,6 +285,6 @@ def get_dof_weight_distribution( asset: Articulation = env.scene[asset_cfg.name] if env_ids is None: env_ids = slice(None) - force_from_child_link_to_joints = asset.root_physx_view.get_link_incoming_joint_force().to(env.device)[env_ids] + force_from_child_link_to_joints = _as_torch(asset.root_view.get_link_incoming_joint_force()).to(env.device)[env_ids] weight_forces = force_from_child_link_to_joints[..., 2] return weight_forces diff --git a/source/uwlab/uwlab/envs/mdp/actions/default_joint_static_action.py b/source/uwlab/uwlab/envs/mdp/actions/default_joint_static_action.py index 37d0c7fd..a6dbdf35 100644 --- a/source/uwlab/uwlab/envs/mdp/actions/default_joint_static_action.py +++ b/source/uwlab/uwlab/envs/mdp/actions/default_joint_static_action.py @@ -27,8 +27,8 @@ def __init__(self, cfg: actions_cfg.DefaultJointPositionStaticActionCfg, env: Ma super().__init__(cfg, env) # use default joint positions as offset if cfg.use_default_offset: - self._offset = self._asset.data.default_joint_pos[:, self._joint_ids].clone() - self._default_actions = self._asset.data.default_joint_pos[:, self._joint_ids].clone() + self._offset = self._asset.data.default_joint_pos.torch[:, self._joint_ids].clone() + self._default_actions = self._asset.data.default_joint_pos.torch[:, self._joint_ids].clone() @property def action_dim(self) -> int: diff --git a/source/uwlab/uwlab/envs/mdp/actions/task_space_actions.py b/source/uwlab/uwlab/envs/mdp/actions/task_space_actions.py index 290e57d1..ce84f112 100644 --- a/source/uwlab/uwlab/envs/mdp/actions/task_space_actions.py +++ b/source/uwlab/uwlab/envs/mdp/actions/task_space_actions.py @@ -131,13 +131,13 @@ def desired_joint_position(self): @property def jacobian_w(self) -> torch.Tensor: - return self._asset.root_physx_view.get_jacobians()[:, self._jacobi_body_idx, :, :][:, :, :, self._joint_ids] + return self._asset.data.body_link_jacobian_w.torch[:, self._jacobi_body_idx, :, :][:, :, :, self._joint_ids] @property def jacobian_b(self) -> torch.Tensor: jacobian = self.jacobian_w B = len(self._jacobi_body_idx) - rot_b = self._asset.data.root_link_quat_w + rot_b = self._asset.data.root_link_quat_w.torch rot_b_m = math_utils.matrix_from_quat(math_utils.quat_inv(rot_b)) rot_b_m = rot_b_m.unsqueeze(1).expand(-1, B, -1, -1).reshape(-1, 3, 3) # [N*B, 3, 3] @@ -166,7 +166,7 @@ def process_actions(self, actions: torch.Tensor): def apply_actions(self): # obtain quantities from simulation ee_pos_curr, ee_quat_curr = self._compute_frame_pose() - joint_pos = self._asset.data.joint_pos[:, self._joint_ids] + joint_pos = self._asset.data.joint_pos.torch[:, self._joint_ids] # compute the delta in joint-space if ee_quat_curr.norm() != 0: jacobian = self._compute_frame_jacobian() @@ -191,7 +191,7 @@ def _compute_frame_pose(self) -> tuple[torch.Tensor, torch.Tensor]: """ # obtain quantities from simulation num_body_idx = len(self._body_idx) - ee_pose_w = self._asset.data.body_link_state_w[:, self._body_idx, :7].view(-1, 7) + ee_pose_w = self._asset.data.body_link_state_w.torch[:, self._body_idx, :7].view(-1, 7) root_pose_w = self._asset.data.root_state_w[:, :7].repeat_interleave(num_body_idx, dim=0) # compute the pose of the body in the root frame ee_pos_b, ee_quat_b = math_utils.subtract_frame_transforms( diff --git a/source/uwlab/uwlab/envs/mdp/actions/visualizable_joint_target_position.py b/source/uwlab/uwlab/envs/mdp/actions/visualizable_joint_target_position.py index 71d56882..dc315de1 100644 --- a/source/uwlab/uwlab/envs/mdp/actions/visualizable_joint_target_position.py +++ b/source/uwlab/uwlab/envs/mdp/actions/visualizable_joint_target_position.py @@ -44,27 +44,28 @@ def apply_actions(self): pass def _set_debug_vis_impl(self, debug_vis: bool): - import isaacsim.core.utils.prims as prim_utils + from isaaclab.sim.utils import find_matching_prim_paths + from isaaclab.sim.utils.legacy import get_prim_at_path from pxr import UsdGeom if debug_vis: if not hasattr(self, "vis_articulation"): if self.cfg.articulation_vis_cfg.name in self._env.scene: self.vis_articulation: Articulation = self._env.scene[self.cfg.articulation_vis_cfg.name] - prims_paths = prim_utils.find_matching_prim_paths(self.vis_articulation.cfg.prim_path) - prims = [prim_utils.get_prim_at_path(prim) for prim in prims_paths] + prims_paths = find_matching_prim_paths(self.vis_articulation.cfg.prim_path) + prims = [get_prim_at_path(prim) for prim in prims_paths] for prim in prims: UsdGeom.Imageable(prim).MakeVisible() else: if hasattr(self, "vis_articulation"): - prims_paths = prim_utils.find_matching_prim_paths(self.vis_articulation.cfg.prim_path) - prims = [prim_utils.get_prim_at_path(prim) for prim in prims_paths] + prims_paths = find_matching_prim_paths(self.vis_articulation.cfg.prim_path) + prims = [get_prim_at_path(prim) for prim in prims_paths] for prim in prims: UsdGeom.Imageable(prim).MakeInvisible() def _debug_vis_callback(self, event): # update the box marker self.vis_articulation.write_joint_state_to_sim( - position=self._asset.data.joint_pos_target, - velocity=torch.zeros_like(self._asset.data.joint_pos_target, device=self.device), + position=self._asset.data.joint_pos_target.torch, + velocity=torch.zeros_like(self._asset.data.joint_pos_target.torch, device=self.device), ) diff --git a/source/uwlab/uwlab/envs/mdp/commands/categorical_command.py b/source/uwlab/uwlab/envs/mdp/commands/categorical_command.py index fd97d189..05d82c7c 100644 --- a/source/uwlab/uwlab/envs/mdp/commands/categorical_command.py +++ b/source/uwlab/uwlab/envs/mdp/commands/categorical_command.py @@ -124,8 +124,8 @@ def _debug_vis_callback(self, event): return # get marker location # -- base state - base_pos_w = self.robot.data.root_pos_w.clone() - base_quat_w = self.robot.data.root_quat_w.clone() + base_pos_w = self.robot.data.root_pos_w.torch.clone() + base_quat_w = self.robot.data.root_quat_w.torch.clone() base_pos_w[:, 2] += 0.5 # -- resolve the scales scale = self.command[:].repeat_interleave(3, 0).view(-1, 3) diff --git a/source/uwlab/uwlab/envs/mdp/events.py b/source/uwlab/uwlab/envs/mdp/events.py index b6add045..e9106f87 100644 --- a/source/uwlab/uwlab/envs/mdp/events.py +++ b/source/uwlab/uwlab/envs/mdp/events.py @@ -20,13 +20,13 @@ def reset_robot_to_default( ): """Reset the scene to the default state specified in the scene configuration.""" robot: Articulation = env.scene[robot_cfg.name] - default_root_state = robot.data.default_root_state[env_ids].clone() + default_root_state = robot.data.default_root_state.torch[env_ids].clone() default_root_state[:, 0:3] += env.scene.env_origins[env_ids] # set into the physics simulation robot.write_root_state_to_sim(default_root_state, env_ids=env_ids) # obtain default joint positions - default_joint_pos = robot.data.default_joint_pos[env_ids].clone() - default_joint_vel = robot.data.default_joint_vel[env_ids].clone() + default_joint_pos = robot.data.default_joint_pos.torch[env_ids].clone() + default_joint_vel = robot.data.default_joint_vel.torch[env_ids].clone() # set into the physics simulation robot.write_joint_state_to_sim(default_joint_pos, default_joint_vel, env_ids=env_ids) @@ -40,14 +40,18 @@ def launch_view_port( position: tuple[int, int] = (0, 0), ): if env.sim.has_gui(): - from isaacsim.core.utils.viewports import create_viewport_for_camera, get_viewport_names + import omni.kit.commands + from omni.kit.viewport.utility import create_viewport_window + from omni.kit.viewport.window import get_viewport_window_instances - if view_portname not in get_viewport_names(): - create_viewport_for_camera( - viewport_name=view_portname, - camera_prim_path=camera_path, + if view_portname not in [window.title for window in get_viewport_window_instances()]: + viewport_window = create_viewport_window( + name=view_portname, width=viewport_size[0], height=viewport_size[1], position_x=position[0], position_y=position[1], ) + omni.kit.commands.execute( + "SetViewportCamera", camera_path=camera_path, viewport_api=viewport_window.viewport_api + ) diff --git a/source/uwlab/uwlab/envs/mdp/rewards.py b/source/uwlab/uwlab/envs/mdp/rewards.py index 6dcb98bf..b7eb1d07 100644 --- a/source/uwlab/uwlab/envs/mdp/rewards.py +++ b/source/uwlab/uwlab/envs/mdp/rewards.py @@ -35,7 +35,7 @@ def joint_position_command_error_l2_norm( """Penalize tracking of the joint position error using L2-norm.""" asset: Articulation = env.scene[asset_cfg.name] command = env.command_manager.get_command(command_name) - cur_joint_position = asset.data.joint_pos[:, asset_cfg.joint_ids] + cur_joint_position = asset.data.joint_pos.torch[:, asset_cfg.joint_ids] error = torch.norm(command - cur_joint_position, dim=1) return error @@ -50,7 +50,7 @@ def link_position_command_align_tanh( # obtain the desired and current positions des_pos_b = command[:, :3] des_pos_w, _ = combine_frame_transforms(asset.data.root_state_w[:, :3], asset.data.root_state_w[:, 3:7], des_pos_b) - curr_pos_w = asset.data.body_link_pos_w[:, asset_cfg.body_ids[0], :3] # type: ignore + curr_pos_w = asset.data.body_link_pos_w.torch[:, asset_cfg.body_ids[0], :3] # type: ignore distance = torch.norm(curr_pos_w - des_pos_w, dim=1) return 1 - torch.tanh(distance / std) @@ -65,7 +65,7 @@ def link_position_command_error_l2_norm( # obtain the desired and current positions des_pos_b = command[:, :3] des_pos_w, _ = combine_frame_transforms(asset.data.root_state_w[:, :3], asset.data.root_state_w[:, 3:7], des_pos_b) - curr_pos_w = asset.data.body_link_pos_w[:, asset_cfg.body_ids[0], :3] # type: ignore + curr_pos_w = asset.data.body_link_pos_w.torch[:, asset_cfg.body_ids[0], :3] # type: ignore return torch.norm(curr_pos_w - des_pos_w, dim=1) @@ -79,7 +79,7 @@ def link_orientation_command_align_tanh( # obtain the desired and current orientations des_quat_b = command[:, 3:7] des_quat_w = quat_mul(asset.data.root_state_w[:, 3:7], des_quat_b) - curr_quat_w = asset.data.body_link_quat_w[:, asset_cfg.body_ids[0]] # type: ignore + curr_quat_w = asset.data.body_link_quat_w.torch[:, asset_cfg.body_ids[0]] # type: ignore return 1 - torch.tanh(quat_error_magnitude(curr_quat_w, des_quat_w) / std) @@ -93,5 +93,5 @@ def link_orientation_command_error_l2_norm( # obtain the desired and current orientations des_quat_b = command[:, 3:7] des_quat_w = quat_mul(asset.data.root_state_w[:, 3:7], des_quat_b) - curr_quat_w = asset.data.body_link_quat_w[:, asset_cfg.body_ids[0]] # type: ignore + curr_quat_w = asset.data.body_link_quat_w.torch[:, asset_cfg.body_ids[0]] # type: ignore return quat_error_magnitude(curr_quat_w, des_quat_w) diff --git a/source/uwlab/uwlab/envs/mdp/terminations.py b/source/uwlab/uwlab/envs/mdp/terminations.py index 0628c3cc..ba4739a5 100644 --- a/source/uwlab/uwlab/envs/mdp/terminations.py +++ b/source/uwlab/uwlab/envs/mdp/terminations.py @@ -29,9 +29,9 @@ def invalid_state(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Te """Return true if the RigidBody position reads nan""" # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] - return torch.isnan(asset.data.body_pos_w).any(dim=-1).any(dim=-1) + return torch.isnan(asset.data.body_pos_w.torch).any(dim=-1).any(dim=-1) def abnormal_robot_state(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: robot: Articulation = env.scene[asset_cfg.name] - return (robot.data.joint_vel.abs() > (robot.data.joint_vel_limits * 2)).any(dim=1) + return (robot.data.joint_vel.torch.abs() > (robot.data.joint_vel_limits.torch * 2)).any(dim=1) diff --git a/source/uwlab/uwlab/envs/real_rl_env.py b/source/uwlab/uwlab/envs/real_rl_env.py index 1db29ee6..a20a623c 100644 --- a/source/uwlab/uwlab/envs/real_rl_env.py +++ b/source/uwlab/uwlab/envs/real_rl_env.py @@ -11,7 +11,6 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Any, ClassVar -import isaacsim.core.utils.torch as torch_utils from isaaclab.envs import ManagerBasedRLEnv from isaaclab.envs.common import VecEnvObs, VecEnvStepReturn from isaaclab.managers import ( @@ -24,6 +23,7 @@ RewardManager, TerminationManager, ) +from isaaclab.utils.seed import configure_seed from isaaclab.utils.timer import Timer if TYPE_CHECKING: @@ -235,7 +235,7 @@ def reset( return self.obs_buf, self.extras def seed(self, seed: int = -1) -> int: - return torch_utils.set_seed(seed) + return configure_seed(seed) def close(self) -> None: del self.command_manager diff --git a/source/uwlab/uwlab/envs/ui/base_env_window.py b/source/uwlab/uwlab/envs/ui/base_env_window.py index cb157144..43a7bf0f 100644 --- a/source/uwlab/uwlab/envs/ui/base_env_window.py +++ b/source/uwlab/uwlab/envs/ui/base_env_window.py @@ -15,8 +15,8 @@ import omni.kit.app import omni.kit.commands import omni.usd +from isaaclab.sim.utils.stage import get_current_stage from isaaclab.ui.widgets import ManagerLiveVisualizer -from isaacsim.core.utils.stage import get_current_stage from pxr import PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics if TYPE_CHECKING: diff --git a/source/uwlab/uwlab/envs/ui/viewport_camera_controller.py b/source/uwlab/uwlab/envs/ui/viewport_camera_controller.py index 3f1ea2e2..3cf83bf0 100644 --- a/source/uwlab/uwlab/envs/ui/viewport_camera_controller.py +++ b/source/uwlab/uwlab/envs/ui/viewport_camera_controller.py @@ -160,7 +160,7 @@ def update_view_to_asset_root(self, asset_name: str): # set origin type to asset_root self.cfg.origin_type = "asset_root" # update the camera origins - self.viewer_origin = self._env.scene[self.cfg.asset_name].data.root_pos_w[self.cfg.env_index] + self.viewer_origin = self._env.scene[self.cfg.asset_name].data.root_pos_w.torch[self.cfg.env_index] # update the camera view self.update_view_location() @@ -193,7 +193,9 @@ def update_view_to_asset_body(self, asset_name: str, body_name: str): # set origin type to asset_body self.cfg.origin_type = "asset_body" # update the camera origins - self.viewer_origin = self._env.scene[self.cfg.asset_name].data.body_pos_w[self.cfg.env_index, body_id].view(3) + self.viewer_origin = ( + self._env.scene[self.cfg.asset_name].data.body_pos_w.torch[self.cfg.env_index, body_id].view(3) + ) # update the camera view self.update_view_location() diff --git a/source/uwlab/uwlab/sim/converters/mesh_converter.py b/source/uwlab/uwlab/sim/converters/mesh_converter.py index 08461efd..5908f303 100644 --- a/source/uwlab/uwlab/sim/converters/mesh_converter.py +++ b/source/uwlab/uwlab/sim/converters/mesh_converter.py @@ -6,13 +6,18 @@ import asyncio import os -import isaacsim.core.utils.prims as prim_utils +import isaaclab.sim.utils.legacy as prim_utils import omni import omni.kit.commands from isaaclab.sim.converters.asset_converter_base import AssetConverterBase from isaaclab.sim.schemas import schemas -from isaaclab.sim.utils import clone, export_prim_to_file, get_all_matching_child_prims, safe_set_attribute_on_usd_prim -from isaacsim.coreutils.extensions import enable_extension +from isaaclab.sim.utils import ( + clone, + enable_extension, + export_prim_to_file, + get_all_matching_child_prims, + safe_set_attribute_on_usd_prim, +) from pxr import Sdf, Usd, UsdGeom, UsdPhysics, UsdShade, UsdUtils from .mesh_converter_cfg import MeshConverterCfg diff --git a/source/uwlab/uwlab/sim/spawners/materials/physics_materials.py b/source/uwlab/uwlab/sim/spawners/materials/physics_materials.py index 9220ff04..59a70750 100644 --- a/source/uwlab/uwlab/sim/spawners/materials/physics_materials.py +++ b/source/uwlab/uwlab/sim/spawners/materials/physics_materials.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING -import isaacsim.core.utils.prims as prim_utils +import isaaclab.sim.utils.legacy as prim_utils from isaaclab.sim.utils import clone, safe_set_attribute_on_usd_schema from pxr import PhysxSchema, Usd, UsdPhysics, UsdShade diff --git a/source/uwlab/uwlab/sim/spawners/materials/visual_materials.py b/source/uwlab/uwlab/sim/spawners/materials/visual_materials.py index adc1cc21..396c97d7 100644 --- a/source/uwlab/uwlab/sim/spawners/materials/visual_materials.py +++ b/source/uwlab/uwlab/sim/spawners/materials/visual_materials.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING -import isaacsim.core.utils.prims as prim_utils +import isaaclab.sim.utils.legacy as prim_utils import omni.kit.commands from isaaclab.sim.utils import clone, safe_set_attribute_on_usd_prim from isaaclab.utils.assets import NVIDIA_NUCLEUS_DIR diff --git a/source/uwlab/uwlab/ui/widgets/line_plot.py b/source/uwlab/uwlab/ui/widgets/line_plot.py index fa51ed70..3e3ddd34 100644 --- a/source/uwlab/uwlab/ui/widgets/line_plot.py +++ b/source/uwlab/uwlab/ui/widgets/line_plot.py @@ -11,7 +11,7 @@ from typing import TYPE_CHECKING import omni -from isaacsim.core.api.simulation_context import SimulationContext +from isaaclab.sim import SimulationContext with suppress(ImportError): # isaacsim.gui is not available when running in headless mode. diff --git a/source/uwlab/uwlab/ui/widgets/manager_live_visualizer.py b/source/uwlab/uwlab/ui/widgets/manager_live_visualizer.py index 3fdea7b9..dfaf23e5 100644 --- a/source/uwlab/uwlab/ui/widgets/manager_live_visualizer.py +++ b/source/uwlab/uwlab/ui/widgets/manager_live_visualizer.py @@ -13,8 +13,8 @@ import omni.kit.app from isaaclab.managers import ManagerBase +from isaaclab.sim import SimulationContext from isaaclab.utils import configclass -from isaacsim.core.api.simulation_context import SimulationContext from .image_plot import ImagePlot from .line_plot import LiveLinePlot diff --git a/source/uwlab/uwlab/utils/datasets/torch_dataset_file_handler.py b/source/uwlab/uwlab/utils/datasets/torch_dataset_file_handler.py index f52442d1..d4385f07 100644 --- a/source/uwlab/uwlab/utils/datasets/torch_dataset_file_handler.py +++ b/source/uwlab/uwlab/utils/datasets/torch_dataset_file_handler.py @@ -61,12 +61,22 @@ def get_num_episodes(self) -> int: """Get number of episodes in the file.""" return self._episode_count - def write_episode(self, episode: EpisodeData, demo_id: int | None = None): + def write_episode(self, episode: EpisodeData, demo_id: int | None = None, dataset_compression: bool = True): """Add an episode to the dataset. Args: episode: The episode data to add. demo_id: Custom index for the episode. If None, uses default index. + dataset_compression: Accepted for interface compatibility and ignored. + Isaac Lab 3.0's RecorderManager passes ``cfg.dataset_compression`` + positionally to every handler (see + :meth:`isaaclab.managers.RecorderManager.export_episodes`); omitting + it raises ``TypeError: write_episode() takes from 2 to 3 positional + arguments but 4 were given`` at the first episode export. It is + meaningful only for the HDF5 handler, which maps it onto gzip + per-dataset compression. This handler accumulates episodes in memory + and serializes them with ``torch.save`` in :meth:`flush`, which has no + equivalent option. """ if episode.is_empty() or not episode.success: return @@ -99,7 +109,8 @@ def load_episode(self, episode_name: str, device: str = "cpu") -> EpisodeData | def flush(self): """Flush any pending data to disk.""" if self._file_path and self._episode_data: - torch.save(self._episode_data, self._file_path) + # Stamp the quaternion convention; loaders refuse files without it. + torch.save({**self._episode_data, "quat_convention": "xyzw"}, self._file_path) def close(self): """Close the dataset file handler.""" diff --git a/source/uwlab/uwlab/utils/datasets/zarr_dataset_file_handler.py b/source/uwlab/uwlab/utils/datasets/zarr_dataset_file_handler.py index 219f4fd5..29ba650d 100644 --- a/source/uwlab/uwlab/utils/datasets/zarr_dataset_file_handler.py +++ b/source/uwlab/uwlab/utils/datasets/zarr_dataset_file_handler.py @@ -199,12 +199,13 @@ def get_num_episodes(self) -> int: """Get number of episodes in the file.""" return self._episode_count - def write_episode(self, episode: EpisodeData, demo_id: int | None = None): + def write_episode(self, episode: EpisodeData, demo_id: int | None = None, dataset_compression: bool = True): """Add an episode to the dataset. Args: episode: The episode data to add. demo_id: Custom index for the episode. If None, uses default index. + dataset_compression: Passed by :class:`isaaclab.managers.RecorderManager`; unused here. """ if self._dataset is None or episode.is_empty(): return diff --git a/source/uwlab_assets/config/extension.toml b/source/uwlab_assets/config/extension.toml index dd819024..bf4b4ff6 100644 --- a/source/uwlab_assets/config/extension.toml +++ b/source/uwlab_assets/config/extension.toml @@ -1,6 +1,6 @@ [package] # Semantic Versioning is used: https://semver.org/ -version = "0.5.2" +version = "0.6.0" # Description title = "UW Lab Assets" diff --git a/source/uwlab_assets/docs/CHANGELOG.rst b/source/uwlab_assets/docs/CHANGELOG.rst index cb39cdbe..fbcca60f 100644 --- a/source/uwlab_assets/docs/CHANGELOG.rst +++ b/source/uwlab_assets/docs/CHANGELOG.rst @@ -1,6 +1,21 @@ Changelog --------- +0.6.0 (2026-09-28) +~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Migrated assets to Isaac Lab 3.0 Early Access / Isaac Sim 6.1 with scalar-last + ``(x, y, z, w)`` quaternions and compatible OmniReset datasets and checkpoints. +* Pinned cloud assets to the canonical ``isaaclab3`` Hugging Face branch's verified + snapshot. Publish new assets on ``isaaclab3`` and update the commit pin deliberately; + ``main`` remains the legacy Isaac Lab 2.x asset line. +* Aligned the USD Python provider using ``usd-exchange``. Use a fresh environment when + upgrading to avoid overlapping ``pxr`` files from ``usd-core``. + + 0.5.2 (2025-03-23) ~~~~~~~~~~~~~~~~~~ diff --git a/source/uwlab_assets/setup.py b/source/uwlab_assets/setup.py index ff0b971b..5a247890 100644 --- a/source/uwlab_assets/setup.py +++ b/source/uwlab_assets/setup.py @@ -17,7 +17,7 @@ # Minimum dependencies required prior to installation INSTALL_REQUIRES = [ - "usd-core", + "usd-exchange==2.3.0", ] # Installation operation diff --git a/source/uwlab_assets/uwlab_assets/__init__.py b/source/uwlab_assets/uwlab_assets/__init__.py index 0bf6a5f1..8e946ace 100644 --- a/source/uwlab_assets/uwlab_assets/__init__.py +++ b/source/uwlab_assets/uwlab_assets/__init__.py @@ -21,23 +21,39 @@ UWLAB_ASSETS_METADATA = toml.load(os.path.join(UWLAB_ASSETS_EXT_DIR, "config", "extension.toml")) """Extension metadata dictionary parsed from the extension.toml file.""" -UWLAB_CLOUD_ASSETS_DIR = "https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main" +UWLAB_CLOUD_ASSETS_REPO = "https://huggingface.co/datasets/UW-Lab/uwlab-assets" +"""HuggingFace dataset repository holding the cloud assets.""" +UWLAB_CLOUD_ASSETS_REVISION = "83860532010b2737aa80d6e8621235ee554186f0" # branch isaaclab3 +"""Pinned commit of :data:`UWLAB_CLOUD_ASSETS_REPO`. -def _extract_relative_path(url: str) -> str: - """Strip the HuggingFace resolve-URL prefix, returning the repo-relative path. +Pinned rather than a branch name so that asset changes on HuggingFace are opt-in: bump this +constant deliberately when new assets or datasets are published. Isaac Lab 3.0 EA / Isaac Sim 6.1 +assets and compatible state checkpoints live on ``isaaclab3``; ``main`` keeps the Isaac Lab 2.x files. +""" + +UWLAB_CLOUD_ASSETS_DIR = f"{UWLAB_CLOUD_ASSETS_REPO}/resolve/{UWLAB_CLOUD_ASSETS_REVISION}" + + +def _extract_revision_and_relative_path(url: str) -> tuple[str, str]: + """Split a HuggingFace resolve URL into ``(revision, repo-relative path)``. Example: - ``https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/main/Props/Custom/Peg/peg.usd`` - -> ``Props/Custom/Peg/peg.usd`` + ``https://huggingface.co/datasets/UW-Lab/uwlab-assets/resolve/isaaclab3/Props/Custom/Peg/peg.usd`` + -> ``("isaaclab3", "Props/Custom/Peg/peg.usd")`` """ parsed = urlparse(url) parts = parsed.path.strip("/").split("/") try: idx = parts.index("resolve") - return "/".join(parts[idx + 2 :]) - except ValueError: - return parsed.path.strip("/") + return parts[idx + 1], "/".join(parts[idx + 2 :]) + except (ValueError, IndexError): + return "", parsed.path.strip("/") + + +def _extract_relative_path(url: str) -> str: + """Strip the HuggingFace resolve-URL prefix, returning the repo-relative path.""" + return _extract_revision_and_relative_path(url)[1] def _urlretrieve_quiet(url: str, dest: str) -> None: @@ -57,15 +73,17 @@ def resolve_cloud_path(path: str) -> str: """Resolve a cloud asset path to a local file, downloading if needed. * Local paths (including already-cached files) are returned immediately. - * HTTPS URLs are downloaded once to ``~/.cache/uwlab/assets/`` - and the local cached path is returned on subsequent calls. + * HTTPS URLs are downloaded once to ``~/.cache/uwlab/assets//`` + and the local cached path is returned on subsequent calls. The revision is part + of the key so that bumping :data:`UWLAB_CLOUD_ASSETS_REVISION` never serves a + file cached from another revision. * Downloads are atomic (write to a temp file, then ``os.rename``). """ if not path.startswith(("http://", "https://")): return path - rel = _extract_relative_path(path) - cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "uwlab", "assets") + revision, rel = _extract_revision_and_relative_path(path) + cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "uwlab", "assets", revision) local = os.path.join(cache_dir, rel) if os.path.isfile(local): diff --git a/source/uwlab_assets/uwlab_assets/robots/franka/action.py b/source/uwlab_assets/uwlab_assets/robots/franka/action.py index 04f16dbe..615cc4a4 100644 --- a/source/uwlab_assets/uwlab_assets/robots/franka/action.py +++ b/source/uwlab_assets/uwlab_assets/robots/franka/action.py @@ -23,7 +23,7 @@ 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.1034), rot=(1.0, 0.0, 0, 0)), + body_offset=DifferentialInverseKinematicsActionCfg.OffsetCfg(pos=(0.0, 0.0, 0.1034), rot=(0.0, 0.0, 0.0, 1.0)), ) JOINT_IKABSOLUTE: DifferentialInverseKinematicsActionCfg = DifferentialInverseKinematicsActionCfg( @@ -32,7 +32,7 @@ body_name="panda_hand", controller=DifferentialIKControllerCfg(command_type="pose", use_relative_mode=False, ik_method="dls"), scale=1, - body_offset=DifferentialInverseKinematicsActionCfg.OffsetCfg(pos=(0.0, 0.0, 0.1034), rot=(1.0, 0.0, 0, 0)), + body_offset=DifferentialInverseKinematicsActionCfg.OffsetCfg(pos=(0.0, 0.0, 0.1034), rot=(0.0, 0.0, 0.0, 1.0)), ) BINARY_GRIPPER = BinaryJointPositionActionCfg( diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/leap.py b/source/uwlab_assets/uwlab_assets/robots/leap/leap.py index 77419545..40aae891 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/leap.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/leap.py @@ -32,7 +32,7 @@ ), joint_drive_props=sim_utils.JointDrivePropertiesCfg(drive_type="force"), ), - init_state=ArticulationCfg.InitialStateCfg(pos=(0, 0, 0), rot=(1, 0, 0, 0), joint_pos=LEAP_DEFAULT_JOINT_POS), + init_state=ArticulationCfg.InitialStateCfg(pos=(0, 0, 0), rot=(0, 0, 0, 1), joint_pos=LEAP_DEFAULT_JOINT_POS), soft_joint_pos_limit_factor=1, ) @@ -67,7 +67,7 @@ name="ee", offset=OffsetCfg( pos=(-0.028, -0.04, -0.07), - rot=(1.0, 0.0, 0.0, 0.0), + rot=(0.0, 0.0, 0.0, 1.0), ), ), ], diff --git a/source/uwlab_assets/uwlab_assets/robots/tycho/actions.py b/source/uwlab_assets/uwlab_assets/robots/tycho/actions.py index d32e3b38..fff9b7ba 100644 --- a/source/uwlab_assets/uwlab_assets/robots/tycho/actions.py +++ b/source/uwlab_assets/uwlab_assets/robots/tycho/actions.py @@ -24,7 +24,7 @@ body_name="static_chop_tip", controller=DifferentialIKControllerCfg(command_type="pose", use_relative_mode=True, ik_method="dls"), scale=0.05, - body_offset=DifferentialInverseKinematicsActionCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0, 0)), + body_offset=DifferentialInverseKinematicsActionCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(0.0, 0.0, 0.0, 1.0)), ) @@ -34,7 +34,7 @@ body_name="static_chop_tip", # Do not work if this is not end_effector controller=DifferentialIKControllerCfg(command_type="pose", use_relative_mode=False, ik_method="dls"), scale=1, - body_offset=DifferentialInverseKinematicsActionCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(1, 0, 0, 0)), + body_offset=DifferentialInverseKinematicsActionCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(0, 0, 0, 1)), ) diff --git a/source/uwlab_assets/uwlab_assets/robots/tycho/tycho.py b/source/uwlab_assets/uwlab_assets/robots/tycho/tycho.py index 560d99c7..872f382b 100644 --- a/source/uwlab_assets/uwlab_assets/robots/tycho/tycho.py +++ b/source/uwlab_assets/uwlab_assets/robots/tycho/tycho.py @@ -42,7 +42,7 @@ enabled_self_collisions=False, solver_position_iteration_count=32, solver_velocity_iteration_count=1 ), ), - init_state=ArticulationCfg.InitialStateCfg(rot=(0.7071068, 0, 0, 0.7071068), joint_pos=HEBI_DEFAULT_JOINTPOS), + init_state=ArticulationCfg.InitialStateCfg(rot=(0, 0, 0.7071068, 0.7071068), joint_pos=HEBI_DEFAULT_JOINTPOS), soft_joint_pos_limit_factor=1, ) @@ -75,7 +75,7 @@ name="ee", offset=OffsetCfg( pos=(0.0, 0.0, 0.0), - rot=(1.0, 0.0, 0.0, 0.0), + rot=(0.0, 0.0, 0.0, 1.0), ), ), ], diff --git a/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/ur5e_robotiq_2f85_gripper.py b/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/ur5e_robotiq_2f85_gripper.py index 5d52f343..9fe9cd1b 100644 --- a/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/ur5e_robotiq_2f85_gripper.py +++ b/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/ur5e_robotiq_2f85_gripper.py @@ -13,12 +13,16 @@ * :obj:`UR5E_ROBOTIQ_2F85`: Alias for ``EXPLICIT_UR5E_ROBOTIQ_2F85`` (backward compatibility). """ +import logging + import isaaclab.sim as sim_utils from isaaclab.actuators import DelayedPDActuatorCfg, ImplicitActuatorCfg from isaaclab.assets.articulation import ArticulationCfg from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR +logger = logging.getLogger(__name__) + ROBOTIQ_2F85_DEFAULT_JOINT_POS = { "finger_joint": 0.0, "right_outer_knuckle_joint": 0.0, @@ -28,6 +32,47 @@ "right_inner_finger_knuckle_joint": 0.0, } + +def spawn_ur5e_without_physics_materials(prim_path, cfg, translation=None, orientation=None): + """Spawn the UR5e USD, then strip its two authored physics materials and their bindings. + + PhysX 110 allocates a PxMaterial per env per USD material (107 shared them), so the pair hits the + hard 64K cap at 32768 envs; ``robot_material`` overwrites the finger friction at startup anyway. + """ + from pxr import PhysxSchema, Sdf, Usd, UsdPhysics, UsdShade + + prim = sim_utils.spawn_from_usd(prim_path, cfg, translation, orientation) + roots = sim_utils.find_matching_prims(prim_path) or [prim] + removed: list[str] = [] + for root in roots: + stage = root.GetStage() + material_paths = { + str(child.GetPath()) + for child in Usd.PrimRange(root) + if child.HasAPI(UsdPhysics.MaterialAPI) or child.HasAPI(PhysxSchema.PhysxMaterialAPI) + } + if not material_paths: + continue + for child in Usd.PrimRange(root): + if not child.HasAPI(UsdShade.MaterialBindingAPI): + continue + binding_api = UsdShade.MaterialBindingAPI(child) + rel = binding_api.GetDirectBindingRel("physics") + if rel and any(str(target) in material_paths for target in rel.GetTargets()): + binding_api.UnbindDirectBinding("physics") + for material_path in sorted(material_paths): + stage.RemovePrim(Sdf.Path(material_path)) + # prim specs authored in the referenced asset survive RemovePrim; deactivating prunes + # them from composition (and from the PhysX parser) instead. + leftover = stage.GetPrimAtPath(Sdf.Path(material_path)) + if leftover.IsValid(): + leftover.SetActive(False) + removed.extend(sorted(material_paths)) + if removed: + logger.info(f"Removed USD physics materials from spawned UR5e (PhysX 64K material cap): {removed}") + return prim + + UR5E_DEFAULT_JOINT_POS = { "shoulder_pan_joint": 0.0, "shoulder_lift_joint": -1.5708, @@ -58,6 +103,7 @@ UR5E_ARTICULATION = ArticulationCfg( spawn=sim_utils.UsdFileCfg( + func=spawn_ur5e_without_physics_materials, usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Robots/UniversalRobots/Ur5e2f85RobotiqGripperCalibrated/ur5e_robotiq_gripper_d415_mount_safety_calibrated.usd", activate_contact_sensors=False, rigid_props=sim_utils.RigidBodyPropertiesCfg( @@ -68,7 +114,7 @@ enabled_self_collisions=True, solver_position_iteration_count=36, solver_velocity_iteration_count=0 ), ), - init_state=ArticulationCfg.InitialStateCfg(pos=(0, 0, 0), rot=(1, 0, 0, 0), joint_pos=UR5E_DEFAULT_JOINT_POS), + init_state=ArticulationCfg.InitialStateCfg(pos=(0, 0, 0), rot=(0, 0, 0, 1), joint_pos=UR5E_DEFAULT_JOINT_POS), soft_joint_pos_limit_factor=1, ) @@ -87,7 +133,7 @@ mass_props=sim_utils.MassPropertiesCfg(mass=0.5), ), init_state=ArticulationCfg.InitialStateCfg( - pos=(0, 0, 0.1), rot=(1, 0, 0, 0), joint_pos=ROBOTIQ_2F85_DEFAULT_JOINT_POS + pos=(0, 0, 0.1), rot=(0, 0, 0, 1), joint_pos=ROBOTIQ_2F85_DEFAULT_JOINT_POS ), actuators={ "gripper": ImplicitActuatorCfg( diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm_leap/xarm_leap.py b/source/uwlab_assets/uwlab_assets/robots/xarm_leap/xarm_leap.py index ffd1f063..65c14c96 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm_leap/xarm_leap.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm_leap/xarm_leap.py @@ -35,7 +35,7 @@ enabled_self_collisions=True, solver_position_iteration_count=1, solver_velocity_iteration_count=0 ), ), - init_state=ArticulationCfg.InitialStateCfg(pos=(0, 0, 0), rot=(1, 0, 0, 0), joint_pos=XARM_LEAP_DEFAULT_JOINT_POS), + init_state=ArticulationCfg.InitialStateCfg(pos=(0, 0, 0), rot=(0, 0, 0, 1), joint_pos=XARM_LEAP_DEFAULT_JOINT_POS), soft_joint_pos_limit_factor=1, ) @@ -77,7 +77,7 @@ name="ee", offset=OffsetCfg( pos=(-0.028, -0.04, -0.07), - rot=(1.0, 0.0, 0.0, 0.0), + rot=(0.0, 0.0, 0.0, 1.0), ), ), ], diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/xarm_uf_gripper.py b/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/xarm_uf_gripper.py index 5e9290c1..d0730203 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/xarm_uf_gripper.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/xarm_uf_gripper.py @@ -37,7 +37,7 @@ ), ), init_state=ArticulationCfg.InitialStateCfg( - pos=(0, 0, 0), rot=(1, 0, 0, 0), joint_pos=XARM_UF_GRIPPER_DEFAULT_JOINT_POS + pos=(0, 0, 0), rot=(0, 0, 0, 1), joint_pos=XARM_UF_GRIPPER_DEFAULT_JOINT_POS ), soft_joint_pos_limit_factor=1, ) diff --git a/source/uwlab_rl/config/extension.toml b/source/uwlab_rl/config/extension.toml index aaba2caa..c81a7449 100644 --- a/source/uwlab_rl/config/extension.toml +++ b/source/uwlab_rl/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.1.4" +version = "0.2.0" # Description title = "UW Lab RL" diff --git a/source/uwlab_rl/docs/CHANGELOG.rst b/source/uwlab_rl/docs/CHANGELOG.rst index 8b81c456..7f95c567 100644 --- a/source/uwlab_rl/docs/CHANGELOG.rst +++ b/source/uwlab_rl/docs/CHANGELOG.rst @@ -1,6 +1,21 @@ Changelog --------- +0.2.0 (2026-09-28) +~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Migrated to the official UW-Lab RSL-RL 5.4.1 release (``uw-v5.4.1``), using separate + actor and critic models with Isaac Lab 3.0 Early Access, Isaac Sim 6.1 and Python 3.12. +* Updated JIT export to support heteroscedastic Gaussian policies and their distributions. + +Run ``./uwlab.sh --install`` to install the pinned dependencies and use the compatible +checkpoints linked in the OmniReset quick start. Use ``isaaclab2`` / ``v1.3.0`` for +legacy RSL-RL 3.x callers, including the combined ``ActorCritic`` interface. + + 0.1.4 (2026-09-14) ~~~~~~~~~~~~~~~~~~ diff --git a/source/uwlab_rl/setup.py b/source/uwlab_rl/setup.py index 34314176..485774b5 100644 --- a/source/uwlab_rl/setup.py +++ b/source/uwlab_rl/setup.py @@ -19,16 +19,27 @@ # Minimum dependencies required prior to installation INSTALL_REQUIRES = [ # generic - "wandb>=0.19.6", + # rsl_rl's WandbSummaryWriter still passes ``wandb.Settings(start_method="thread")``; + # wandb removed that field and its Settings model rejects unknown fields, so newer + # releases fail at the first log call. 0.19.x is the last series that accepts it. + "wandb>=0.19.6,<0.20", ] PYTORCH_INDEX_URL = ["https://download.pytorch.org/whl/cu118"] # Extra dependencies for RL agents +# Pinned to a commit, not a branch: an unpinned git dependency makes a rebuild +# silently install a different API than the one this code was written against. +# Must be a commit on UW-Lab/rsl_rl with the rsl-rl >= 5.0 API that Isaac Lab 3.0's +# isaaclab_rl requires, including HeteroscedasticGaussianDistribution (rsl-rl 5.3). +# Bump together with the Isaac Lab commit pinned in uwlab.sh. +# Released UW-Lab/rsl_rl integration after UW-Lab/rsl_rl#6 merged. +RSL_RL_REPO = "https://github.com/UW-Lab/rsl_rl.git" +RSL_RL_COMMIT = "2c3bf18001a5e2a78527e9ea368b7ea31700a2c5" # uw-v5.4.1 (UW-Lab/rsl_rl#6) EXTRAS_REQUIRE = { "rsl-rl": [ # Update this pin alongside compatible UWLab changes. - "rsl-rl-lib @ git+https://github.com/UW-Lab/rsl_rl.git@e7cd3c77bdb3c94753612f208c725e1add38a655", + f"rsl-rl-lib @ git+{RSL_RL_REPO}@{RSL_RL_COMMIT}", ], } @@ -48,15 +59,15 @@ keywords=EXTENSION_TOML_DATA["package"]["keywords"], license="BSD-3-Clause", include_package_data=True, - python_requires=">=3.10", + python_requires=">=3.12,<3.13", install_requires=INSTALL_REQUIRES, dependency_links=PYTORCH_INDEX_URL, extras_require=EXTRAS_REQUIRE, packages=["uwlab_rl"], classifiers=[ "Natural Language :: English", - "Programming Language :: Python :: 3.10", - "Isaac Sim :: 4.5.0", + "Programming Language :: Python :: 3.12", + "Isaac Sim :: 6.1.0", ], zip_safe=False, ) diff --git a/source/uwlab_rl/uwlab_rl/rsl_rl/exporter.py b/source/uwlab_rl/uwlab_rl/rsl_rl/exporter.py index b0df39dd..1e3855ec 100644 --- a/source/uwlab_rl/uwlab_rl/rsl_rl/exporter.py +++ b/source/uwlab_rl/uwlab_rl/rsl_rl/exporter.py @@ -3,183 +3,105 @@ # # SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause +"""JIT export of an rsl-rl >= 5.0 actor with its output distribution. + +rsl-rl's own ``OnPolicyRunner.export_policy_to_jit`` exports the mean action only. Demo collection +(``scripts_v2/tools/collect_demos.py``) samples from the expert, so the exported module also needs +``compute_distribution(obs) -> (mean, std)``. Fixed-std Gaussians have their std evaluated once at export +time; heteroscedastic Gaussians (std predicted by the MLP next to the mean) and gSDE compute it per observation +with the same clamping as the distribution, so all of them export exactly. +""" +from __future__ import annotations + +import copy import os import torch from torch import nn -from isaaclab_rl.rsl_rl.exporter import _OnnxPolicyExporter, _TorchPolicyExporter +from rsl_rl.modules import HeteroscedasticGaussianDistribution -def export_policy_as_jit(policy: object, normalizer: object | None, path: str, filename="policy.pt"): - """Export policy into a Torch JIT file. +def export_policy_as_jit(actor: nn.Module, path: str, filename: str = "policy.pt") -> None: + """Export an rsl-rl ``MLPModel`` actor into a TorchScript file with ``forward`` and ``compute_distribution``. Args: - policy: The policy torch module. - normalizer: The empirical normalizer module. If None, Identity is used. - path: The path to the saving directory. - filename: The name of exported JIT file. Defaults to "policy.pt". + actor: The actor model (``rsl_rl.models.MLPModel``), uncompiled. + path: The directory to save into. + filename: The file name. Defaults to "policy.pt". """ - policy_exporter = _TorchPolicyExporterExtended(policy, normalizer) - policy_exporter.export(path, filename) - - -def export_policy_as_onnx( - policy: object, path: str, normalizer: object | None = None, filename="policy.onnx", verbose=False -): - """Export policy into a Torch ONNX file. - - Args: - policy: The policy torch module. - normalizer: The empirical normalizer module. If None, Identity is used. - path: The path to the saving directory. - filename: The name of exported ONNX file. Defaults to "policy.onnx". - verbose: Whether to print the model summary. Defaults to False. - """ - if not os.path.exists(path): - os.makedirs(path, exist_ok=True) - policy_exporter = _OnnxPolicyExporterExtended(policy, normalizer, verbose) - policy_exporter.export(path, filename) - - -""" -Helper Classes - Private. -""" - - -class _StateDependentPolicyMixin(nn.Module): - """Mixin class to handle state-dependent policy logic.""" - - def _setup_state_dependent_policy(self, policy): - """Setup state-dependent policy components.""" - self.actor_features = self.actor[:-1] # type: ignore - self.actor_final = self.actor[-1] # type: ignore - - self.register_buffer("log_std", policy.log_std.clone()) + os.makedirs(path, exist_ok=True) + exporter = _TorchPolicyExporter(actor).to("cpu") # device-neutral artifact; consumers move it as needed + torch.jit.script(exporter).save(os.path.join(path, filename)) + + +class _TorchPolicyExporter(nn.Module): + """TorchScript-able snapshot of an ``MLPModel`` actor: normalizer, MLP and output distribution.""" + + def __init__(self, actor: nn.Module) -> None: + super().__init__() + self.normalizer = copy.deepcopy(actor.obs_normalizer) + # ``MLP`` is an ``nn.Sequential`` subclass whose ``__init__`` takes positional arguments, + # so it cannot be sliced; rebuild the split around the last layer explicitly. + layers = [copy.deepcopy(layer) for layer in actor.mlp] + self.actor_features = nn.Sequential(*layers[:-1]) + self.actor_final = layers[-1] self.epsilon = 1e-6 - - def _setup_regular_policy(self, policy): - """Setup regular policy components.""" - self.actor_features = self.actor[:-1] # type: ignore - self.actor_final = self.actor[-1] # type: ignore - - if hasattr(policy, "std"): - self.register_buffer("std", policy.std.clone()) - if hasattr(policy, "log_std"): - self.register_buffer("log_std", policy.log_std.clone()) - if hasattr(policy, "noise_std_type"): - self.noise_std_type = policy.noise_std_type - else: - self.noise_std_type = "scalar" - - # For GSDE, ensure epsilon is set - if self.noise_std_type == "gsde": - self.epsilon = 1e-6 - - def _ensure_compatibility_attributes(self, policy): - """Ensure all attributes exist for TorchScript compatibility.""" - if not hasattr(self, "std"): - if hasattr(policy, "std"): - self.register_buffer("std", policy.std.clone()) - else: - # Create a default std tensor - default_std = torch.ones(policy.num_actions if hasattr(policy, "num_actions") else 1) - self.register_buffer("std", default_std) - - if not hasattr(self, "log_std"): - if hasattr(policy, "log_std"): - self.register_buffer("log_std", policy.log_std.clone()) + self.heteroscedastic = False + self.gsde = False + self.log_std = False + self.std_min, self.std_max = 0.0, 0.0 + self.log_std_min, self.log_std_max = 0.0, 0.0 + + dist = actor.distribution + with torch.no_grad(): + if dist is None: + self.register_buffer("std", torch.ones(1)) + self.register_buffer("std_matrix", torch.ones(1, 1)) + elif isinstance(dist, HeteroscedasticGaussianDistribution): + # The MLP outputs [..., 2, num_actions]: mean, then (log-)std, clamped like ``dist.update``. + self.heteroscedastic = True + self.log_std = dist.std_type == "log" + self.std_min, self.std_max = float(dist.std_range[0]), float(dist.std_range[1]) + self.log_std_min, self.log_std_max = float(dist.log_std_range[0]), float(dist.log_std_range[1]) + self.register_buffer("std", torch.ones(1)) + self.register_buffer("std_matrix", torch.ones(1, 1)) + elif hasattr(dist, "_get_std") and getattr(dist, "requires_latent_sde", False): + # gSDE: marginal std = sqrt(phi(s)^2 @ std_matrix^2), std_matrix is (latent_dim, num_actions) + # after the distribution's own clamp / full_std / expln handling. + self.gsde = True + self.register_buffer("std", torch.ones(1)) + self.register_buffer("std_matrix", dist._get_std().detach().clone()) else: - # Create a default log_std tensor - default_log_std = torch.zeros(policy.num_actions if hasattr(policy, "num_actions") else 1) - self.register_buffer("log_std", default_log_std) + # Gaussian: evaluate the clamped std once from the distribution's parameterization. + if getattr(dist, "std_type", "scalar") == "log": + std = torch.exp(dist.log_std_param.clamp(dist.log_std_range[0], dist.log_std_range[1])) + else: + std = dist.std_param.clamp(dist.std_range[0], dist.std_range[1]) + self.register_buffer("std", std.detach().clone()) + self.register_buffer("std_matrix", torch.ones(1, 1)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + out = self.actor_final(self.actor_features(self.normalizer(x))) + if self.heteroscedastic: + return out[..., 0, :] + return out - if not hasattr(self, "epsilon"): - self.epsilon = 1e-6 - - if not hasattr(self, "noise_std_type"): - if hasattr(policy, "noise_std_type"): - self.noise_std_type = policy.noise_std_type + @torch.jit.export + def compute_distribution(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + features = self.actor_features(self.normalizer(x)) + out = self.actor_final(features) + if self.heteroscedastic: + mean = out[..., 0, :] + if self.log_std: + std = torch.exp(out[..., 1, :].clamp(self.log_std_min, self.log_std_max)) else: - self.noise_std_type = "scalar" # Default fallback - - # Ensure epsilon is set for GSDE - if self.noise_std_type == "gsde" and not hasattr(self, "epsilon"): - self.epsilon = 1e-6 - - def _compute_distribution(self, observations): - """Compute mean and std for distribution.""" - if self.is_state_dependent.item(): # type: ignore - # Use the separated layers - features = self.actor_features(observations) # type: ignore - mean = self.actor_final(features) # type: ignore - - # Compute variance using exploration matrices and torch.mm - variance = torch.mm(features**2, torch.exp(self.log_std) ** 2) # type: ignore + std = out[..., 1, :].clamp(self.std_min, self.std_max) + elif self.gsde: + mean = out + variance = torch.mm(features**2, self.std_matrix**2) std = torch.sqrt(variance + self.epsilon) - - return mean, std - else: - # Regular ActorCritic logic - mean = self.actor(observations) # type: ignore - - if self.noise_std_type == "scalar": - std = self.std.expand_as(mean) # type: ignore - elif self.noise_std_type == "log": - std = torch.exp(self.log_std).expand_as(mean) # type: ignore - elif self.noise_std_type == "gsde": - # GSDE: log_std is a matrix (hidden_dim, num_actions) - # Compute features from actor[:-1] (all layers except last) - features = self.actor_features(observations) # type: ignore - # Compute variance: variance = torch.mm(features**2, exp(log_std)**2) - # features shape: (batch, hidden_dim), log_std shape: (hidden_dim, num_actions) - variance = torch.mm(features**2, torch.exp(self.log_std) ** 2) # type: ignore - std = torch.sqrt(variance + self.epsilon) - else: - std = torch.ones_like(mean) - - return mean, std - - -class _TorchPolicyExporterExtended(_TorchPolicyExporter, _StateDependentPolicyMixin): - def __init__(self, policy, normalizer=None): - super().__init__(policy, normalizer) - - # Detect policy type - is_state_dependent = hasattr(policy, "use_state_dependent_noise") and policy.use_state_dependent_noise - self.register_buffer("is_state_dependent", torch.tensor(is_state_dependent, dtype=torch.bool)) - - if is_state_dependent: - self._setup_state_dependent_policy(policy) - else: - self._setup_regular_policy(policy) - - # Ensure all attributes exist for TorchScript compatibility - self._ensure_compatibility_attributes(policy) - - @torch.jit.export - def compute_distribution(self, x): - observations = self.normalizer(x) - return self._compute_distribution(observations) - - -class _OnnxPolicyExporterExtended(_OnnxPolicyExporter, _StateDependentPolicyMixin): - def __init__(self, policy, normalizer=None, verbose=False): - super().__init__(policy, normalizer, verbose) - - is_state_dependent = hasattr(policy, "use_state_dependent_noise") and policy.use_state_dependent_noise - self.register_buffer("is_state_dependent", torch.tensor(is_state_dependent, dtype=torch.bool)) - - if is_state_dependent: - self._setup_state_dependent_policy(policy) else: - self._setup_regular_policy(policy) - - @torch.jit.export - def compute_distribution(self, x): - observations = self.normalizer(x) - return self._compute_distribution(observations) + mean = out + std = self.std.expand_as(mean) + return mean, std diff --git a/source/uwlab_rl/uwlab_rl/rsl_rl/rl_cfg.py b/source/uwlab_rl/uwlab_rl/rsl_rl/rl_cfg.py index 11917539..795d50a5 100644 --- a/source/uwlab_rl/uwlab_rl/rsl_rl/rl_cfg.py +++ b/source/uwlab_rl/uwlab_rl/rsl_rl/rl_cfg.py @@ -4,10 +4,9 @@ # SPDX-License-Identifier: BSD-3-Clause from dataclasses import MISSING -from typing import Literal from isaaclab.utils import configclass -from isaaclab_rl.rsl_rl import RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg # noqa: F401 +from isaaclab_rl.rsl_rl import RslRlMLPModelCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg # noqa: F401 @configclass @@ -57,15 +56,12 @@ class OffPolicyAlgorithmCfg: """The configuration for the offline behavior cloning(dagger).""" -@configclass -class RslRlFancyActorCriticCfg(RslRlPpoActorCriticCfg): - """Configuration for the fancy actor-critic networks.""" - - state_dependent_std: bool = False - """Whether to use state-dependent standard deviation.""" - - noise_std_type: Literal["scalar", "log", "gsde"] = "scalar" - """The type of noise standard deviation for the policy. Default is scalar.""" +# Must stay an alias, not a subclass: isaaclab_rl's `policy` -> `actor`/`critic` shim +# dispatches on `type(cfg.policy) is RslRlPpoActorCriticCfg`, so a subclass falls through +# every branch and leaves `actor` MISSING (surfacing as `KeyError: 'class_name'` in +# PPO.construct_algorithm). See isaaclab_rl/rsl_rl/utils.py. +RslRlFancyActorCriticCfg = RslRlPpoActorCriticCfg +"""Alias of :class:`RslRlPpoActorCriticCfg`; see the note above.""" @configclass diff --git a/source/uwlab_tasks/config/extension.toml b/source/uwlab_tasks/config/extension.toml index 37dbd4f7..b6b67257 100644 --- a/source/uwlab_tasks/config/extension.toml +++ b/source/uwlab_tasks/config/extension.toml @@ -1,7 +1,7 @@ [package] # Semantic Versioning is used: https://semver.org/ -version = "0.13.8" +version = "0.14.0" # Description title = "UW Lab Tasks" diff --git a/source/uwlab_tasks/docs/CHANGELOG.rst b/source/uwlab_tasks/docs/CHANGELOG.rst index 44050103..dd0fb8a2 100644 --- a/source/uwlab_tasks/docs/CHANGELOG.rst +++ b/source/uwlab_tasks/docs/CHANGELOG.rst @@ -1,6 +1,35 @@ Changelog --------- +0.14.0 (2026-09-28) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Migrated tasks to Isaac Lab 3.0 Early Access / Isaac Sim 6.1, including ``sim.physics`` + configuration, scalar-last quaternions, Warp-backed data and ``JointWrenchSensor``. + Use ``--visualizer none`` instead of ``--headless`` to disable visualization. +* Adopted the EA observation order and compatible OmniReset datasets and checkpoints. + Use the pretrained experts linked in the updated quick start. +* Deferred task imports until selection while preserving existing task IDs and entry points. +* Preserved authored or geometry-derived object masses during grasp sampling instead of + requesting an ineffective 1 g override. Verify mass properties for custom assets. + +Fixed +^^^^^ + +* Corrected OmniReset and Factory velocity observations to rotate vectors into the root + frame without subtracting the robot's position. +* Fixed partial resets clearing consecutive-success counters in unrelated environments. +* Corrected grasp-sampling asset resolution, collider frames and geometry-cache hashes, + and restored backend-aware velocity-stability filters. Regenerate affected grasp and + reset-state datasets to apply these fixes. +* Preserved initialized joint armature when applying ADR. +* Fixed renderer setup for physics-replicated scenes. +* Batched OBB corner computation and debug drawing without changing the termination decision. + + 0.13.8 (2025-10-24) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/source/uwlab_tasks/setup.py b/source/uwlab_tasks/setup.py index ef5632e8..7e4dbacb 100644 --- a/source/uwlab_tasks/setup.py +++ b/source/uwlab_tasks/setup.py @@ -26,27 +26,31 @@ "cmaes", ] +# pytorch3d is only needed by the collision analyzer, which runs in the +# dataset-generation tasks (reset states, grasp sampling, partial assemblies). +# omnireset/mdp/utils.py imports it lazily so the RL tasks stay usable without it, +# and it is exposed as the ``collision`` extra (``uwlab.sh -i`` installs it). +# +# There is no sdist fallback on purpose -- building pytorch3d from source needs a +# matching CUDA toolkit and takes a long time -- so a wheel is only added when a +# prebuilt one exists for this python/CUDA combination. The build is tied to the +# torch it was compiled against, so keep this table in sync with the torch pin in +# uwlab.sh (`ensure_cuda_torch`) and check a new pin actually loads. +# +# cp310/cp311 -> 0.7.8 + pt2.7.0 + cu128 (Isaac Lab 2.3.2 stack) +# cp312 -> 0.7.9 + pt2.10.0 + cu128 (Isaac Lab 3.0 stack) +# +# cp312 needs 0.7.9: 0.7.8's newest cp312 build is pt2.8.0. No pt2.11 build is +# published for either release, so cp312 takes the newest (pt2.10.0); it loads +# against the torch 2.11.0 that Isaac Sim 6.0.1 pins (CUDA ops verified). is_linux_x86_64 = platform.system() == "Linux" and platform.machine() in ("x86_64", "AMD64") py = f"cp{sys.version_info.major}{sys.version_info.minor}" wheel_by_py = { - "cp311": ( - "https://github.com/MiroPsota/torch_packages_builder/releases/download/pytorch3d-0.7.8/" - "pytorch3d-0.7.8%2Bpt2.7.0cu128-cp311-cp311-linux_x86_64.whl" - ), - "cp310": ( - "https://github.com/MiroPsota/torch_packages_builder/releases/download/pytorch3d-0.7.8/" - "pytorch3d-0.7.8%2Bpt2.7.0cu128-cp310-cp310-linux_x86_64.whl" + "cp312": ( + "https://github.com/MiroPsota/torch_packages_builder/releases/download/pytorch3d-0.7.9/" + "pytorch3d-0.7.9%2Bpt2.10.0cu128-cp312-cp312-linux_x86_64.whl" ), -} - -if is_linux_x86_64 and py in wheel_by_py: - INSTALL_REQUIRES.append(f"pytorch3d @ {wheel_by_py[py]}") - -is_linux_x86_64 = platform.system() == "Linux" and platform.machine() in ("x86_64", "AMD64") -py = f"cp{sys.version_info.major}{sys.version_info.minor}" - -wheel_by_py = { "cp311": ( "https://github.com/MiroPsota/torch_packages_builder/releases/download/pytorch3d-0.7.8/" "pytorch3d-0.7.8%2Bpt2.7.0cu128-cp311-cp311-linux_x86_64.whl" @@ -57,8 +61,9 @@ ), } -if is_linux_x86_64 and py in wheel_by_py: - INSTALL_REQUIRES.append(f"pytorch3d @ {wheel_by_py[py]}") +EXTRAS_REQUIRE = { + "collision": [f"pytorch3d @ {wheel_by_py[py]}"] if is_linux_x86_64 and py in wheel_by_py else [], +} # Installation operation setup( @@ -73,6 +78,7 @@ include_package_data=True, python_requires=">=3.10", install_requires=INSTALL_REQUIRES, + extras_require=EXTRAS_REQUIRE, packages=["uwlab_tasks"], classifiers=[ "Natural Language :: English", diff --git a/source/uwlab_tasks/test/test_environments.py b/source/uwlab_tasks/test/test_environments.py index 2916244b..d5147f5e 100644 --- a/source/uwlab_tasks/test/test_environments.py +++ b/source/uwlab_tasks/test/test_environments.py @@ -15,8 +15,20 @@ """Rest everything follows.""" +import importlib +import math +import torch +from itertools import product +from types import SimpleNamespace +from unittest.mock import Mock + import pytest from env_test_utils import _run_environments, setup_environment +from isaaclab.assets import BaseArticulation, BaseRigidObject, BaseRigidObjectCollection +from isaaclab.managers import EventManager, ObservationManager, ObservationTermCfg, SceneEntityCfg +from isaaclab.sensors import CameraCfg +from isaaclab.sim.utils import find_matching_prim_paths, get_all_matching_child_prims +from pxr import Usd, UsdGeom, UsdPhysics import uwlab_tasks # noqa: F401 @@ -27,3 +39,427 @@ def test_environments(task_name, num_envs, device): # run environments without stage in memory _run_environments(task_name, device, num_envs, create_stage_in_memory=False) + + +@pytest.mark.parametrize("task_family", ["omnireset", "factory_extension"]) +@pytest.mark.parametrize("body_ids", [slice(None), [1]]) +@pytest.mark.parametrize("stationary", [True, False]) +@pytest.mark.parametrize("translated", [True, False]) +@pytest.mark.isaacsim_ci +def test_asset_link_velocity_frame_transform(task_family, body_ids, stationary, translated): + mdp = importlib.import_module(f"uwlab_tasks.manager_based.manipulation.{task_family}.mdp.observations") + positions = torch.tensor([[1.0, 2.0, 3.0], [-4.0, 7.0, 9.0], [25.0, -6.0, 0.5]]) + if not translated: + positions.zero_() + half_sqrt = math.sqrt(0.5) + quaternions = torch.tensor([[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, half_sqrt, half_sqrt], [1.0, 0.0, 0.0, 0.0]]) + linear = torch.tensor([[[1.0, 2.0, 3.0], [7.0, 8.0, 9.0]]]).repeat(3, 1, 1) + angular = torch.tensor([[[4.0, 5.0, 6.0], [10.0, 11.0, 12.0]]]).repeat(3, 1, 1) + if stationary: + linear.zero_() + angular.zero_() + target = SimpleNamespace( + data=SimpleNamespace( + body_lin_vel_w=SimpleNamespace(torch=linear), body_ang_vel_w=SimpleNamespace(torch=angular) + ) + ) + root = SimpleNamespace( + data=SimpleNamespace( + root_pos_w=SimpleNamespace(torch=positions), root_quat_w=SimpleNamespace(torch=quaternions) + ) + ) + env = SimpleNamespace(scene={"target": target, "robot": root}) + actual = mdp.asset_link_velocity_in_root_asset_frame(env, SceneEntityCfg("target", body_ids=body_ids)) + index = 0 if isinstance(body_ids, slice) else body_ids[0] + linear_selected, angular_selected = linear[:, index], angular[:, index] + expected = torch.cat([linear_selected, angular_selected], dim=-1) + expected[1] = expected[1, [1, 0, 2, 4, 3, 5]] * torch.tensor([1.0, -1.0, 1.0, 1.0, -1.0, 1.0]) + expected[2] *= torch.tensor([1.0, -1.0, -1.0, 1.0, -1.0, -1.0]) + torch.testing.assert_close(actual, expected, rtol=1e-5, atol=1e-5) + + +@pytest.mark.isaacsim_ci +def test_omnireset_renderer_configuration(): + mdp = importlib.import_module("uwlab_tasks.manager_based.manipulation.omnireset.mdp.events") + front = CameraCfg(prim_path="/World/Front", spawn=None, width=16, height=16) + side = CameraCfg(prim_path="/World/Side", spawn=None, width=16, height=16) + cfg = SimpleNamespace(scene=SimpleNamespace(front=front, side=side, robot=object()), events=SimpleNamespace()) + settings = { + "enable_dlssg": False, + "enable_reflections": True, + "enable_ambient_occlusion": True, + "enable_dl_denoiser": True, + "antialiasing_mode": "DLAA", + } + mdp.configure_isaac_rtx(cfg, **settings) + event = cfg.events.render_settings + assert event.mode == "startup" + assert event.func is mdp.apply_isaac_rtx_settings + for camera in (front, side): + assert camera.renderer_cfg.renderer_type == "isaac_rtx" + assert camera.renderer_cfg.enable_scene_partitioning is False + for key, value in settings.items(): + assert getattr(camera.renderer_cfg.global_settings, key) == value + front.renderer_cfg.global_settings.enable_reflections = False + assert side.renderer_cfg.global_settings.enable_reflections is True + assert event.params["settings"].enable_reflections is True + + +@pytest.mark.isaacsim_ci +def test_omnireset_renderer_configuration_without_cameras(): + mdp = importlib.import_module("uwlab_tasks.manager_based.manipulation.omnireset.mdp.events") + cfg = SimpleNamespace(scene=SimpleNamespace(robot=object()), events=SimpleNamespace()) + mdp.configure_isaac_rtx(cfg, enable_dlssg=True) + mdp.configure_isaac_rtx(cfg, enable_dlssg=False) + assert list(vars(cfg.events)) == ["render_settings"] + assert cfg.events.render_settings.params["settings"].enable_dlssg is False + + +@pytest.mark.isaacsim_ci +def test_omnireset_renderer_configuration_with_replicated_scene(): + mdp = importlib.import_module("uwlab_tasks.manager_based.manipulation.omnireset.mdp.events") + scene = SimpleNamespace(replicate_physics=True) + cfg = SimpleNamespace(scene=scene, events=SimpleNamespace()) + mdp.configure_isaac_rtx(cfg, enable_dlssg=True) + handle = SimpleNamespace(deregister=lambda: None) + sim = SimpleNamespace( + is_playing=lambda: False, + physics_manager=SimpleNamespace(register_callback=lambda *args, **kwargs: handle), + ) + env = SimpleNamespace(scene=SimpleNamespace(cfg=scene), sim=sim, num_envs=1, device="cpu") + manager = EventManager(cfg.events, env) + assert manager.available_modes == ["startup"] + assert scene.replicate_physics is True + + +@pytest.mark.isaacsim_ci +def test_omnireset_checkpoint_observation_layout(): + module = importlib.import_module( + "uwlab_tasks.manager_based.manipulation.omnireset.config.ur5e_robotiq_2f85.rl_state_cfg" + ) + names = [ + "prev_actions", + "joint_pos", + "end_effector_pose", + "insertive_asset_pose", + "receptive_asset_pose", + "insertive_asset_in_receptive_asset_frame", + ] + widths = [7, 12, 6, 6, 6, 6] + for group_type in (module.ObservationsCfg.PolicyCfg, module.ObservationsCfg.CriticCfg): + group = group_type() + terms = [name for name, value in vars(group).items() if isinstance(value, ObservationTermCfg)] + assert terms[:6] == names + + def sentinel(env, width, value): + return torch.full((env.num_envs, width), value, device=env.device) + + group = module.ObservationsCfg.PolicyCfg() + for index, (name, width) in enumerate(zip(names, widths)): + term = getattr(group, name) + term.func = sentinel + term.params = {"width": width, "value": float(index + 1)} + term.noise = None + env = SimpleNamespace(num_envs=2, device="cpu", sim=SimpleNamespace(is_playing=lambda: True)) + manager = ObservationManager({"policy": group}, env) + expected = torch.cat([torch.full((2, width), float(index + 1)) for index, width in enumerate(widths)], dim=-1) + assert manager.active_terms["policy"] == names + torch.testing.assert_close(manager.compute()["policy"], expected, rtol=0, atol=0) + + +@pytest.mark.parametrize("env_ids, expected", [([1], [4, 0]), ([], [4, 2]), (None, [0, 0]), ([0, 1], [0, 0])]) +@pytest.mark.isaacsim_ci +def test_progress_context_reset_is_per_environment(env_ids, expected): + module = importlib.import_module("uwlab_tasks.manager_based.manipulation.omnireset.mdp.rewards") + context = module.ProgressContext.__new__(module.ProgressContext) + context.continuous_success_counter = torch.tensor([4, 2], dtype=torch.int32) + reference = context.continuous_success_counter + ids = None if env_ids is None else torch.tensor(env_ids, dtype=torch.long) + context.reset(ids) + assert context.continuous_success_counter is reference + assert torch.equal(reference, torch.tensor(expected, dtype=torch.int32)) + context.reset(ids) + assert torch.equal(reference, torch.tensor(expected, dtype=torch.int32)) + + +@pytest.mark.parametrize("pattern", ["/World/envs/env_.*/Object", "/World/envs/env_[^/]+/Object"]) +@pytest.mark.isaacsim_ci +def test_collision_asset_paths_and_frames(monkeypatch, pattern): + module = importlib.import_module("uwlab_tasks.manager_based.manipulation.omnireset.mdp.rigid_object_hasher") + stage = Usd.Stage.CreateInMemory() + for index in (10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11): + root = f"/World/envs/env_{index}/Object" + UsdGeom.Xform.Define(stage, root) + for name, y in (("a", 1.0), ("b", float(index + 2))): + cube = UsdGeom.Cube.Define(stage, f"{root}/{name}") + UsdPhysics.CollisionAPI.Apply(cube.GetPrim()) + cube.AddTranslateOp().Set((0.0, y, 0.0)) + monkeypatch.setattr(module.stage_utils, "get_current_stage", lambda: stage) + monkeypatch.setattr(module.stage_utils, "get_current_stage_id", lambda: None) + monkeypatch.setattr(module, "HASH_STORE", {"warp_mesh_store": {}, "__stage_id__": None}) + monkeypatch.setattr( + module, + "find_matching_prim_paths", + lambda expr, stage=None: find_matching_prim_paths(expr, stage=stage), + raising=False, + ) + monkeypatch.setattr( + module, + "get_all_matching_child_prims", + lambda path, **kwargs: get_all_matching_child_prims(path, stage=stage, **kwargs), + ) + hasher = module.RigidObjectHasher(12, pattern, device="cpu") + paths = [str(prim.GetPath()) for prim in hasher.collider_prims] + assert paths == [f"/World/envs/env_{index}/Object/{name}" for index in range(12) for name in ("a", "b")] + expected_quaternions = torch.tensor([[0.0, 0.0, 0.0, 1.0]]).expand(24, -1) + torch.testing.assert_close(hasher.collider_prim_relative_transforms[:, 3:7], expected_quaternions, rtol=0, atol=0) + assert hasher.root_prim_hashes[0] != hasher.root_prim_hashes[1] + + +@pytest.mark.parametrize("term_name", ["check_grasp_success", "check_reset_state_success"]) +@pytest.mark.parametrize("object_kind", ["rigid", "collection"]) +@pytest.mark.isaacsim_ci +def test_dataset_success_requires_backend_asset_stability(term_name, object_kind): + module = importlib.import_module("uwlab_tasks.manager_based.manipulation.omnireset.mdp.terminations") + positions = torch.tensor([[0.0, 0.0, 0.1]]).repeat(4, 1) + quaternions = torch.tensor([[0.0, 0.0, 0.0, 1.0]]).repeat(4, 1) + joint_velocities = torch.zeros(4, 2) + linear_velocities = torch.zeros(4, 2, 3) + angular_velocities = torch.zeros(4, 2, 3) + joint_velocities[1, 0] = 6.0 + linear_velocities[2, 1, 0] = 0.2 + angular_velocities[3, 1, 0] = 2.0 + robot = Mock(spec=BaseArticulation) + robot.initial_pos = positions.clone() + robot.data = SimpleNamespace( + joint_vel=SimpleNamespace(torch=joint_velocities), + joint_vel_limits=SimpleNamespace(torch=torch.full_like(joint_velocities, 100.0)), + root_pos_w=SimpleNamespace(torch=positions), + root_quat_w=SimpleNamespace(torch=quaternions), + body_link_pos_w=SimpleNamespace(torch=positions[:, None]), + body_link_quat_w=SimpleNamespace(torch=quaternions[:, None]), + ) + obj = Mock(spec=BaseRigidObject if object_kind == "rigid" else BaseRigidObjectCollection) + obj.initial_pos = positions.clone() + obj.data = SimpleNamespace( + root_pos_w=SimpleNamespace(torch=positions), + root_quat_w=SimpleNamespace(torch=quaternions), + body_lin_vel_w=SimpleNamespace(torch=linear_velocities), + body_ang_vel_w=SimpleNamespace(torch=angular_velocities), + object_lin_vel_w=SimpleNamespace(torch=linear_velocities), + object_ang_vel_w=SimpleNamespace(torch=angular_velocities), + ) + env = SimpleNamespace( + scene={"robot": robot, "object": obj}, + num_envs=4, + device="cpu", + episode_length_buf=torch.full((4,), 10), + max_episode_length=10, + ) + term_type = getattr(module, term_name) + term = term_type.__new__(term_type) + term.stability_counter = torch.zeros(4, dtype=torch.int32) + term.consecutive_stability_steps = 2 + term.pos_z_threshold = 0.05 + + def collision_free(env, ids): + return torch.ones(len(ids), dtype=torch.bool) + + robot_cfg = SceneEntityCfg("robot") + object_cfg = SceneEntityCfg("object") + if term_name == "check_grasp_success": + term.object_cfg = object_cfg + term.gripper_cfg = robot_cfg + term.max_pos_deviation = 0.05 + term.collision_analyzer = collision_free + params = {"object_cfg": object_cfg, "gripper_cfg": robot_cfg, "collision_analyzer_cfg": None} + else: + term.robot_asset = robot + term.assets_to_check = [obj, robot] + term.ee_body_idx = 0 + term.gripper_approach_direction = (0.0, 0.0, -1.0) + term.max_robot_pos_deviation = term.max_object_pos_deviation = 0.05 + term.collision_analyzers = [collision_free] + term.assembly_success_prob = None + params = { + "object_cfgs": [object_cfg], + "robot_cfg": robot_cfg, + "ee_body_name": "tool", + "collision_analyzer_cfgs": [], + } + assert not term(env, **params).any() + torch.testing.assert_close(term(env, **params), torch.tensor([True, False, False, False])) + torch.testing.assert_close(term.stability_counter, torch.tensor([2, 0, 0, 0], dtype=torch.int32)) + joint_velocities.zero_() + linear_velocities.zero_() + angular_velocities.zero_() + torch.testing.assert_close(term(env, **params), torch.tensor([True, False, False, False])) + assert term(env, **params).all() + linear_velocities[0, 1, 0] = 0.2 + torch.testing.assert_close(term(env, **params), torch.tensor([False, True, True, True])) + assert term.stability_counter[0] == 0 + + +@pytest.mark.isaacsim_ci +def test_grasp_sampling_preserves_asset_masses(): + module = importlib.import_module( + "uwlab_tasks.manager_based.manipulation.omnireset.config.ur5e_robotiq_2f85.grasp_sampling_cfg" + ) + assert module.GraspSamplingSceneCfg().object.spawn.mass_props is None + assert set(module.variants["scene.object"]) == {"peg", "cube", "cupcake", "rectangle", "fbleg", "fbdrawerbottom"} + for object_cfg in module.variants["scene.object"].values(): + assert object_cfg.spawn.mass_props is None + assert object_cfg.spawn.rigid_props.disable_gravity is False + + +@pytest.mark.parametrize("contract", ["asset", "armature"]) +@pytest.mark.isaacsim_ci +def test_omnireset_published_expert_robot_defaults(contract): + if contract == "asset": + module = importlib.import_module("uwlab_assets.robots.ur5e_robotiq_gripper.ur5e_robotiq_2f85_gripper") + assert module.UR5E_ARTICULATION.spawn.usd_path.endswith( + "/ur5e_robotiq_gripper_d415_mount_safety_calibrated.usd" + ) + else: + module = importlib.import_module( + "uwlab_tasks.manager_based.manipulation.omnireset.config.ur5e_robotiq_2f85.rl_state_cfg" + ) + assert module.BaseEventCfg().robot_wrist_armature is None + + +@pytest.mark.isaacsim_ci +def test_sysid_armature_startup_selects_wrist_joints(monkeypatch): + module = importlib.import_module("uwlab_tasks.manager_based.manipulation.omnireset.mdp.events") + names = [ + "shoulder_pan_joint", + "shoulder_lift_joint", + "elbow_joint", + "wrist_1_joint", + "wrist_2_joint", + "wrist_3_joint", + ] + nominal = [3.0, 1.2, 1.4, 0.17, 0.08, 0.38] + written = [] + robot = SimpleNamespace( + cfg=SimpleNamespace(spawn=SimpleNamespace(usd_path="fixture/robot.usd")), + device="cpu", + joint_names=names, + find_joints=lambda selected: ([names.index(name) for name in selected], selected), + write_joint_armature_to_sim_index=lambda **kwargs: written.append(kwargs), + ) + monkeypatch.setattr(module.utils, "read_metadata_from_usd_directory", lambda path: {"sysid": {"armature": nominal}}) + env = SimpleNamespace(scene={"robot": robot}, num_envs=4) + ids = torch.tensor([1, 3]) + module.set_armature_from_sysid(env, ids, SceneEntityCfg("robot", joint_names=names[3:])) + assert written[0]["joint_ids"] == [3, 4, 5] + assert torch.equal(written[0]["env_ids"], ids) + torch.testing.assert_close(written[0]["armature"], torch.tensor([nominal[3:], nominal[3:]]), rtol=0, atol=0) + + +@pytest.mark.isaacsim_ci +def test_armature_curriculum_preserves_startup_baseline(): + module = importlib.import_module("uwlab_tasks.manager_based.manipulation.omnireset.mdp.events") + initial = torch.tensor([[0.0, 0.0, 0.0, 0.17, 0.08, 0.38]]).repeat(4, 1) + current = initial.clone() + written = [] + + def write_armature(armature, joint_ids, env_ids): + current[env_ids[:, None], joint_ids] = armature + written.append(armature.clone()) + + robot = SimpleNamespace( + device="cpu", + data=SimpleNamespace(joint_armature=SimpleNamespace(torch=current)), + actuators={"arm": SimpleNamespace()}, + write_joint_armature_to_sim_index=write_armature, + write_joint_friction_coefficient_to_sim_index=lambda **kwargs: None, + ) + term = module.randomize_arm_from_sysid.__new__(module.randomize_arm_from_sysid) + term.robot = robot + term.joint_ids = list(range(6)) + term.actuator_name = "arm" + term.armature = [3.0, 1.2, 1.4, 0.17, 0.08, 0.38] + term.static_friction = term.dynamic_ratio = term.viscous_friction = [1.0] * 6 + term._initial_armature = None + ids = torch.tensor([1, 3]) + for progress in (0.0, 1.0, 0.5): + term.scale_progress = progress + term(None, ids, None, [], "arm", scale_range=(1.0, 1.0), delay_range=(0, 0)) + expected = initial[ids] * (1.0 - progress) + torch.tensor(term.armature).repeat(2, 1) * progress + torch.testing.assert_close(written[-1], expected, rtol=0, atol=0) + torch.testing.assert_close(current[[0, 2]], initial[[0, 2]], rtol=0, atol=0) + + +@pytest.mark.parametrize("num_envs", [0, 1, 7]) +@pytest.mark.parametrize("dtype", [torch.float32, torch.float64]) +@pytest.mark.isaacsim_ci +def test_obb_corners_stay_batched_on_device(monkeypatch, num_envs, dtype): + module = importlib.import_module("uwlab_tasks.manager_based.manipulation.omnireset.mdp.terminations") + term = module.check_obb_no_overlap_termination.__new__(module.check_obb_no_overlap_termination) + generator = torch.Generator().manual_seed(42) + centroids = torch.randn(num_envs, 3, generator=generator, dtype=dtype, requires_grad=True) + axes = torch.randn(num_envs, 3, 3, generator=generator, dtype=dtype, requires_grad=True) + extents = torch.tensor([0.3, 0.0 if num_envs == 1 else 0.7, 1.2], dtype=dtype) + expected = torch.empty(num_envs, 8, 3, dtype=torch.float32) + for index in range(num_envs): + for corner, signs in enumerate(product((-1, 1), repeat=3)): + expected[index, corner] = centroids[index] + sum( + signs[axis] * extents[axis] * axes[index, axis] for axis in range(3) + ) + with monkeypatch.context() as guard: + guard.setattr(torch.Tensor, "cpu", Mock(side_effect=AssertionError("OBB math transferred to host"))) + guard.setattr(torch.Tensor, "numpy", Mock(side_effect=AssertionError("OBB math used NumPy"))) + actual = term._compute_obb_corners_batch(centroids, axes, extents) + assert actual.device == centroids.device and actual.dtype == torch.float32 and not actual.requires_grad + torch.testing.assert_close(actual, expected, rtol=1e-5, atol=1e-6) + + +@pytest.mark.parametrize("num_envs", [0, 1, 7]) +@pytest.mark.isaacsim_ci +def test_obb_wireframes_transfer_one_batch(monkeypatch, num_envs): + module = importlib.import_module("uwlab_tasks.manager_based.manipulation.omnireset.mdp.terminations") + term = module.check_obb_no_overlap_termination.__new__(module.check_obb_no_overlap_termination) + corners = torch.arange(num_envs * 24, dtype=torch.float32).reshape(num_envs, 8, 3) + draw = Mock() + transfers = [] + original = torch.Tensor.cpu + + def copy_to_cpu(tensor, *args, **kwargs): + transfers.append(tensor.shape) + return original(tensor, *args, **kwargs) + + with monkeypatch.context() as guard: + guard.setattr(torch.Tensor, "cpu", copy_to_cpu) + guard.setattr(torch.Tensor, "numpy", Mock(side_effect=AssertionError("Per-edge NumPy conversion"))) + term._draw_obb_wireframe(corners[0] if num_envs == 1 else corners, draw_interface=draw) + assert len(transfers) == 1 + draw.draw_lines.assert_called_once() + starts, ends, colors, widths = draw.draw_lines.call_args.args + assert len(starts) == len(ends) == len(colors) == len(widths) == 24 * num_envs + for index in range(num_envs): + assert starts[24 * index] == corners[index, 0].tolist() + assert ends[24 * index] == corners[index, 1].tolist() + + +@pytest.mark.isaacsim_ci +def test_obb_visualization_submits_whole_environment_batches(): + module = importlib.import_module("uwlab_tasks.manager_based.manipulation.omnireset.mdp.terminations") + term = module.check_obb_no_overlap_termination.__new__(module.check_obb_no_overlap_termination) + env = SimpleNamespace(num_envs=5) + positions = torch.zeros(5, 3) + quaternions = torch.tensor([[0.0, 0.0, 0.0, 1.0]]).repeat(5, 1) + term.insertive_object = SimpleNamespace( + data=SimpleNamespace( + root_pos_w=SimpleNamespace(torch=positions), root_quat_w=SimpleNamespace(torch=quaternions) + ) + ) + term._insertive_initial_pos, term._insertive_initial_quat = positions, quaternions + term._insertive_obb_centroid = torch.zeros(3) + term._insertive_obb_axes = torch.eye(3) + term._insertive_obb_half_extents = torch.ones(3) + term._omni_debug_draw = SimpleNamespace(acquire_debug_draw_interface=lambda: Mock()) + term._compute_obb_corners_batch = lambda centroids, axes, extents: centroids[:, None, :].expand(-1, 8, -1) + term._draw_obb_wireframe = Mock() + term._visualize_bounding_boxes(env) + assert term._draw_obb_wireframe.call_count == 2 + assert all(call.args[0].shape == (5, 8, 3) for call in term._draw_obb_wireframe.call_args_list) diff --git a/source/uwlab_tasks/test/test_task_registration.py b/source/uwlab_tasks/test/test_task_registration.py new file mode 100644 index 00000000..68a7ad09 --- /dev/null +++ b/source/uwlab_tasks/test/test_task_registration.py @@ -0,0 +1,56 @@ +# Copyright (c) 2024-2026, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import os +import subprocess +import sys + + +def test_task_registration_without_simulator(): + """Register discoverable task IDs without importing their MDP implementations.""" + program = """ +import ast +import importlib.abc +import sys +from pathlib import Path +from unittest.mock import patch + +import gymnasium as gym +from isaaclab.app import AppLauncher + +class RejectTaskMdp(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname.startswith('uwlab_tasks.') and '.mdp' in fullname: + raise AssertionError(f'Task registration eagerly imported {fullname}') + +sys.meta_path.insert(0, RejectTaskMdp()) +with patch.object(AppLauncher, '__init__', side_effect=AssertionError('Simulator launch forbidden')): + import uwlab_tasks + +expected = set() +root = Path(uwlab_tasks.__file__).parent +for path in root.rglob('__init__.py'): + parents = [parent for parent in path.parents if parent != root and root in parent.parents] + if not all((parent / '__init__.py').is_file() for parent in parents): + continue + for node in ast.walk(ast.parse(path.read_text())): + if isinstance(node, ast.Call) and ast.unparse(node.func) == 'gym.register': + for keyword in node.keywords: + if keyword.arg == 'id' and isinstance(keyword.value, ast.Constant): + expected.add(keyword.value.value) +assert expected +assert expected <= set(gym.registry), sorted(expected - set(gym.registry)) +assert 'OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0' in expected +assert not any(name.startswith('uwlab_tasks.') and '.mdp' in name for name in sys.modules) +print(f'Registered {len(expected)} declared task IDs without loading their MDPs') +""" + result = subprocess.run( + [sys.executable, "-c", program], + env={**os.environ, "CUDA_VISIBLE_DEVICES": ""}, + capture_output=True, + text=True, + timeout=60, + ) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/source/uwlab_tasks/uwlab_tasks/__init__.py b/source/uwlab_tasks/uwlab_tasks/__init__.py index ccfcba43..0eee885a 100644 --- a/source/uwlab_tasks/uwlab_tasks/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/__init__.py @@ -25,6 +25,6 @@ from isaaclab_tasks.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/uwlab_tasks/uwlab_tasks/direct/cartpole/cartpole_camera_env.py b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/cartpole_camera_env.py index 31c0704f..86aa4b3e 100644 --- a/source/uwlab_tasks/uwlab_tasks/direct/cartpole/cartpole_camera_env.py +++ b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/cartpole_camera_env.py @@ -38,7 +38,7 @@ class CartpoleRGBCameraEnvCfg(DirectRLEnvCfg): # camera tiled_camera: TiledCameraCfg = TiledCameraCfg( prim_path="/World/envs/env_.*/Camera", - offset=TiledCameraCfg.OffsetCfg(pos=(-5.0, 0.0, 2.0), rot=(1.0, 0.0, 0.0, 0.0), convention="world"), + offset=TiledCameraCfg.OffsetCfg(pos=(-5.0, 0.0, 2.0), rot=(0.0, 0.0, 0.0, 1.0), convention="world"), data_types=["rgb"], spawn=sim_utils.PinholeCameraCfg( focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) @@ -76,7 +76,7 @@ class CartpoleDepthCameraEnvCfg(CartpoleRGBCameraEnvCfg): # camera tiled_camera: TiledCameraCfg = TiledCameraCfg( prim_path="/World/envs/env_.*/Camera", - offset=TiledCameraCfg.OffsetCfg(pos=(-5.0, 0.0, 2.0), rot=(1.0, 0.0, 0.0, 0.0), convention="world"), + offset=TiledCameraCfg.OffsetCfg(pos=(-5.0, 0.0, 2.0), rot=(0.0, 0.0, 0.0, 1.0), convention="world"), data_types=["depth"], spawn=sim_utils.PinholeCameraCfg( focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py index ab441009..b52ca861 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py @@ -24,7 +24,7 @@ class CartpoleRGBCameraSceneCfg(CartpoleSceneCfg): # add camera to the scene tiled_camera: TiledCameraCfg = TiledCameraCfg( prim_path="{ENV_REGEX_NS}/Camera", - offset=TiledCameraCfg.OffsetCfg(pos=(-7.0, 0.0, 3.0), rot=(0.9945, 0.0, 0.1045, 0.0), convention="world"), + offset=TiledCameraCfg.OffsetCfg(pos=(-7.0, 0.0, 3.0), rot=(0.0, 0.1045, 0.0, 0.9945), convention="world"), data_types=["rgb"], spawn=sim_utils.PinholeCameraCfg( focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) @@ -40,7 +40,7 @@ class CartpoleDepthCameraSceneCfg(CartpoleSceneCfg): # add camera to the scene tiled_camera: TiledCameraCfg = TiledCameraCfg( prim_path="{ENV_REGEX_NS}/Camera", - offset=TiledCameraCfg.OffsetCfg(pos=(-7.0, 0.0, 3.0), rot=(0.9945, 0.0, 0.1045, 0.0), convention="world"), + offset=TiledCameraCfg.OffsetCfg(pos=(-7.0, 0.0, 3.0), rot=(0.0, 0.1045, 0.0, 0.9945), convention="world"), data_types=["distance_to_camera"], spawn=sim_utils.PinholeCameraCfg( focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/advance_skills_base_env.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/advance_skills_base_env.py index 7e0ba91f..1c30ee16 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/advance_skills_base_env.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/advance_skills_base_env.py @@ -20,7 +20,8 @@ from isaaclab.terrains import TerrainGeneratorCfg, TerrainImporterCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR -from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise +from isaaclab.utils.noise import UniformNoiseCfg as Unoise +from isaaclab_physx.physics import PhysxCfg import uwlab_tasks.manager_based.locomotion.advance_skills.mdp as mdp @@ -347,10 +348,12 @@ def __post_init__(self): self.sim.dt = 0.005 self.sim.render_interval = self.decimation self.sim.physics_material = self.scene.terrain.physics_material - self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**24 - self.sim.physx.gpu_found_lost_pairs_capacity = 2**24 - self.sim.physx.gpu_collision_stack_size = 2**27 - self.sim.physx.gpu_max_rigid_patch_count = 5 * 2**16 + if self.sim.physics is None: + self.sim.physics = PhysxCfg() + self.sim.physics.gpu_total_aggregate_pairs_capacity = 2**24 + self.sim.physics.gpu_found_lost_pairs_capacity = 2**24 + self.sim.physics.gpu_collision_stack_size = 2**27 + self.sim.physics.gpu_max_rigid_patch_count = 5 * 2**16 # update sensor update periods # we tick all the sensors based on the smallest update period (physics update period) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/mdp/rewards.py index f5b6bd1e..d8684359 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/mdp/rewards.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/mdp/rewards.py @@ -32,7 +32,7 @@ def reward_forward_velocity( """Reward tracking of linear velocity commands (xy axes) using exponential kernel.""" # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] - root_lin_vel_b = asset.data.root_lin_vel_b + root_lin_vel_b = asset.data.root_lin_vel_b.torch forward_velocity = root_lin_vel_b[:, 0] current_iter = int(env.common_step_counter / 48) distance = torch.norm(env.command_manager.get_command("goal_point")[:, :2], dim=1) @@ -53,14 +53,14 @@ def air_time_reward( if contact_sensor.cfg.track_air_time is False: raise RuntimeError("Activate ContactSensor's track_air_time!") # compute the reward - current_air_time = contact_sensor.data.current_air_time[:, sensor_cfg.body_ids] - current_contact_time = contact_sensor.data.current_contact_time[:, sensor_cfg.body_ids] + current_air_time = contact_sensor.data.current_air_time.torch[:, sensor_cfg.body_ids] + current_contact_time = contact_sensor.data.current_contact_time.torch[:, sensor_cfg.body_ids] t_max = torch.max(current_air_time, current_contact_time) t_min = torch.clip(t_max, max=mode_time) stance_cmd_reward = torch.clip(current_contact_time - current_air_time, -mode_time, mode_time) distance = torch.norm(env.command_manager.get_command("goal_point")[:, :2], dim=1).unsqueeze(dim=1).expand(-1, 4) - body_vel = torch.linalg.norm(asset.data.root_com_lin_vel_b[:, :2], dim=1).unsqueeze(dim=1).expand(-1, 4) + body_vel = torch.linalg.norm(asset.data.root_com_lin_vel_b.torch[:, :2], dim=1).unsqueeze(dim=1).expand(-1, 4) reward = torch.where( torch.logical_or(distance > 0.4, body_vel > velocity_threshold), torch.where(t_max < mode_time, t_min, 0), @@ -174,8 +174,8 @@ def air_time_variance_penalty(env: ManagerBasedRLEnv, sensor_cfg: SceneEntityCfg if contact_sensor.cfg.track_air_time is False: raise RuntimeError("Activate ContactSensor's track_air_time!") # compute the reward - last_air_time = contact_sensor.data.last_air_time[:, sensor_cfg.body_ids] - last_contact_time = contact_sensor.data.last_contact_time[:, sensor_cfg.body_ids] + last_air_time = contact_sensor.data.last_air_time.torch[:, sensor_cfg.body_ids] + last_contact_time = contact_sensor.data.last_contact_time.torch[:, sensor_cfg.body_ids] return torch.var(torch.clip(last_air_time, max=0.5), dim=1) + torch.var( torch.clip(last_contact_time, max=0.5), dim=1 ) @@ -190,9 +190,9 @@ def foot_slip_penalty( contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] # check if contact force is above threshold - net_contact_forces = contact_sensor.data.net_forces_w_history + net_contact_forces = contact_sensor.data.net_forces_w_history.torch is_contact = torch.max(torch.norm(net_contact_forces[:, :, sensor_cfg.body_ids], dim=-1), dim=1)[0] > threshold - foot_planar_velocity = torch.linalg.norm(asset.data.body_com_lin_vel_w[:, asset_cfg.body_ids, :2], dim=2) + foot_planar_velocity = torch.linalg.norm(asset.data.body_com_lin_vel_w.torch[:, asset_cfg.body_ids, :2], dim=2) reward = is_contact * foot_planar_velocity return torch.sum(reward, dim=1) @@ -205,6 +205,6 @@ def joint_position_penalty( # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] distance = torch.norm(env.command_manager.get_command("goal_point")[:, :2], dim=1) - body_vel = torch.linalg.norm(asset.data.root_lin_vel_b[:, :2], dim=1) - reward = torch.linalg.norm((asset.data.joint_pos - asset.data.default_joint_pos), dim=1) + body_vel = torch.linalg.norm(asset.data.root_lin_vel_b.torch[:, :2], dim=1) + reward = torch.linalg.norm((asset.data.joint_pos.torch - asset.data.default_joint_pos.torch), dim=1) return torch.where((distance > 0.4) | (body_vel > velocity_threshold), reward, stand_still_scale * reward) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/rewards.py index 873d7960..9c54c45d 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/rewards.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/rewards.py @@ -54,7 +54,7 @@ def heading_tracking(env: ManagerBasedRLEnv, distance_threshold: float = 2.0, re def exploration_reward(env: ManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot")): # Retrieve the robot and target data robot: Articulation = env.scene[robot_cfg.name] - base_velocity = robot.data.root_lin_vel_b # Robot's current base velocity vector + base_velocity = robot.data.root_lin_vel_b.torch # Robot's current base velocity vector target_position = env.command_manager.get_command("goal_point")[ :, :2 ] # Target position vector relative to robot base @@ -79,14 +79,14 @@ def stall_penalty( distance_threshold: float = 0.5, ): robot: Articulation = env.scene[robot_cfg.name] - base_vel = robot.data.root_lin_vel_b.norm(2, dim=-1) + base_vel = robot.data.root_lin_vel_b.torch.norm(2, dim=-1) distance_to_goal = env.command_manager.get_command("goal_point")[:, :2].norm(2, dim=-1) return (base_vel < base_vel_threshold) & (distance_to_goal > distance_threshold) def illegal_contact_penalty(env: ManagerBasedRLEnv, threshold: float, sensor_cfg: SceneEntityCfg): contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] # type: ignore - net_contact_forces = contact_sensor.data.net_forces_w_history + net_contact_forces = contact_sensor.data.net_forces_w_history.torch # check if any contact force exceeds the threshold return torch.any( torch.max(torch.norm(net_contact_forces[:, :, sensor_cfg.body_ids], dim=-1), dim=1)[0] > threshold, @@ -96,13 +96,13 @@ def illegal_contact_penalty(env: ManagerBasedRLEnv, threshold: float, sensor_cfg def feet_lin_acc_l2(env: ManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot")): robot: Articulation = env.scene[robot_cfg.name] - feet_acc = torch.sum(torch.square(robot.data.body_lin_acc_w[..., robot_cfg.body_ids, :]), dim=(1, 2)) + feet_acc = torch.sum(torch.square(robot.data.body_lin_acc_w.torch[..., robot_cfg.body_ids, :]), dim=(1, 2)) return feet_acc def feet_rot_acc_l2(env: ManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot")): robot: Articulation = env.scene[robot_cfg.name] - feet_acc = torch.sum(torch.square(robot.data.body_ang_acc_w[..., robot_cfg.body_ids, :]), dim=(1, 2)) + feet_acc = torch.sum(torch.square(robot.data.body_ang_acc_w.torch[..., robot_cfg.body_ids, :]), dim=(1, 2)) return feet_acc @@ -112,6 +112,6 @@ def stand_penalty( robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: robot: Articulation = env.scene[robot_cfg.name] - base_height = robot.data.root_link_pos_w[:, 2] # z-coordinate of the base + base_height = robot.data.root_link_pos_w.torch[:, 2] # z-coordinate of the base penalty = (base_height < height_threshold).float() * -1.0 return penalty diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/balance_beams_env.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/balance_beams_env.py index 2689e6b0..c362944b 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/balance_beams_env.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/balance_beams_env.py @@ -5,7 +5,7 @@ from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.utils import configclass -from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise +from isaaclab.utils.noise import UniformNoiseCfg as Unoise import uwlab_tasks.manager_based.locomotion.risky_terrains.mdp as mdp diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/rewards.py index 724e6050..c52744ab 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/rewards.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/rewards.py @@ -17,8 +17,8 @@ def joint_vel_limit_pen( env: ManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), limits_factor: float = 0.9 ): robot: Articulation = env.scene[robot_cfg.name] - joint_vel = robot.data.joint_vel[:, robot_cfg.joint_ids] - joint_vel_limit = robot.data.soft_joint_vel_limits[:, robot_cfg.joint_ids] + joint_vel = robot.data.joint_vel.torch[:, robot_cfg.joint_ids] + joint_vel_limit = robot.data.soft_joint_vel_limits.torch[:, robot_cfg.joint_ids] return torch.sum((joint_vel.abs() - limits_factor * joint_vel_limit).clamp_min_(0), dim=-1) @@ -28,8 +28,8 @@ def base_accel_pen( ratio: float = 0.02, ): robot: Articulation = env.scene[robot_cfg.name] - base_angle_accel = robot.data.body_ang_acc_w[:, robot_cfg.body_ids].norm(2, dim=-1).pow(2) - base_lin_accel = robot.data.body_lin_acc_w[:, robot_cfg.body_ids].norm(2, dim=-1).pow(2) + base_angle_accel = robot.data.body_ang_acc_w.torch[:, robot_cfg.body_ids].norm(2, dim=-1).pow(2) + base_lin_accel = robot.data.body_lin_acc_w.torch[:, robot_cfg.body_ids].norm(2, dim=-1).pow(2) return (base_lin_accel + ratio * base_angle_accel).squeeze(-1) @@ -38,7 +38,7 @@ def feet_accel_l1_pen( robot_cfg: SceneEntityCfg = SceneEntityCfg("robot", body_names=".*FOOT"), ): robot: Articulation = env.scene[robot_cfg.name] - feet_acc = robot.data.body_lin_acc_w[:, robot_cfg.body_ids].norm(2, dim=-1) + feet_acc = robot.data.body_lin_acc_w.torch[:, robot_cfg.body_ids].norm(2, dim=-1) return torch.sum(feet_acc, dim=-1) @@ -46,7 +46,7 @@ def contact_forces_pen( env: ManagerBasedRLEnv, threshold: float = 700, sensor_cfg: SceneEntityCfg = SceneEntityCfg("contact_sensor") ): contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] - net_contact_forces = contact_sensor.data.net_forces_w_history + net_contact_forces = contact_sensor.data.net_forces_w_history.torch force = torch.norm(net_contact_forces[:, 0, sensor_cfg.body_ids], dim=-1) return torch.clamp(force - threshold, 0, threshold).pow(2).sum(-1) @@ -80,7 +80,7 @@ def dont_wait( ): robot: Articulation = env.scene[robot_cfg.name] dist_to_goal = env.command_manager.get_command("target_cmd")[:, :2].norm(2, -1) - return (dist_to_goal > d).float() * (robot.data.root_lin_vel_w.norm(2, -1) < velocity_threshold).float() + return (dist_to_goal > d).float() * (robot.data.root_lin_vel_w.torch.norm(2, -1) < velocity_threshold).float() def move_in_dir( @@ -89,7 +89,7 @@ def move_in_dir( robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ): robot: Articulation = env.scene[robot_cfg.name] - lin_vel_b = robot.data.root_lin_vel_b[:, :2] + lin_vel_b = robot.data.root_lin_vel_b.torch[:, :2] target_dir = env.command_manager.get_command("target_cmd")[:, :2] current_iter = int(env.common_step_counter / 48) return torch.cosine_similarity(lin_vel_b, target_dir, dim=-1) * float(current_iter < max_iter) @@ -97,7 +97,9 @@ def move_in_dir( def foot_on_ground(env: ManagerBasedRLEnv, sensor_cfg: SceneEntityCfg, d: float = 0.25, tr: float = 1.0): contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] - foot_on_ground_rew = 1 - torch.tanh(contact_sensor.data.current_air_time[:, sensor_cfg.body_ids].sum(-1) / 0.5) + foot_on_ground_rew = 1 - torch.tanh( + contact_sensor.data.current_air_time.torch[:, sensor_cfg.body_ids].sum(-1) / 0.5 + ) distance_succ_mask = (env.command_manager.get_command("target_cmd")[:, :2].norm(2, -1)) < d rew_window_scaler = ( @@ -120,7 +122,9 @@ def stand_still( tr: float = 1.0, ): robot: Articulation = env.scene[robot_cfg.name] - movement_penalty = 2.5 * robot.data.root_lin_vel_w.norm(2, -1) + 1.0 * robot.data.root_ang_vel_w.norm(2, -1) + movement_penalty = 2.5 * robot.data.root_lin_vel_w.torch.norm(2, -1) + 1.0 * robot.data.root_ang_vel_w.torch.norm( + 2, -1 + ) heading_succ_mask = (env.command_manager.get_command("target_cmd")[:, 3].abs()) < phi distance_succ_mask = (env.command_manager.get_command("target_cmd")[:, :2].norm(2, -1)) < d @@ -138,7 +142,7 @@ def stand_still( def illegal_contact_penalty(env: ManagerBasedRLEnv, threshold: float, sensor_cfg: SceneEntityCfg): contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] - net_contact_forces = contact_sensor.data.net_forces_w_history + net_contact_forces = contact_sensor.data.net_forces_w_history.torch # check if any contact force exceeds the threshold return torch.any( torch.max(torch.norm(net_contact_forces[:, :, sensor_cfg.body_ids], dim=-1), dim=1)[0] > threshold, dim=1 @@ -221,7 +225,7 @@ def aggressive_motion( robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ): robot: Articulation = env.scene[robot_cfg.name] - horizontal_velocity = robot.data.root_lin_vel_w[:, :2].norm(2, -1) + horizontal_velocity = robot.data.root_lin_vel_w.torch[:, :2].norm(2, -1) return (horizontal_velocity - threshold).pow(2) * (horizontal_velocity > threshold).float() @@ -236,7 +240,10 @@ def stand_pos( ): robot: Articulation = env.scene[robot_cfg.name] return ( - ((robot.data.root_pos_w[:, -1] - base_height).abs() + robot.data.projected_gravity_b[:, :2].pow(2).sum(-1)) + ( + (robot.data.root_pos_w.torch[:, -1] - base_height).abs() + + robot.data.projected_gravity_b.torch[:, :2].pow(2).sum(-1) + ) * ((env.command_manager.get_command("target_cmd")[:, :2].norm(2, -1)) < d).float() * ( 1 @@ -257,7 +264,7 @@ def torque_limits( ratio: float = 1.0, ) -> torch.Tensor: asset: Articulation = env.scene[asset_cfg.name] - computed_torque = asset.data.computed_torque[:, asset_cfg.joint_ids].abs() # shape: [batch, joint] + computed_torque = asset.data.computed_torque.torch[:, asset_cfg.joint_ids].abs() # shape: [batch, joint] limits = ratio * asset.actuators.get(actuator_name).effort_limit out_of_limits = torch.clamp(computed_torque - limits, min=0) return torch.sum(out_of_limits, dim=1) @@ -270,8 +277,8 @@ def torque_limits_knee( env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ratio: float = 1.0 ) -> torch.Tensor: asset: Articulation = env.scene[asset_cfg.name] - computed_torque = asset.data.computed_torque[:, asset_cfg.joint_ids] # shape: [batch, joint] - applied_torque = asset.data.applied_torque[:, asset_cfg.joint_ids] + computed_torque = asset.data.computed_torque.torch[:, asset_cfg.joint_ids] # shape: [batch, joint] + applied_torque = asset.data.applied_torque.torch[:, asset_cfg.joint_ids] out_of_limits = ratio * (computed_torque - applied_torque).abs() return torch.sum(out_of_limits, dim=1) @@ -288,7 +295,7 @@ def reward_forward_velocity( """Reward tracking of linear velocity commands (xy axes) using exponential kernel.""" # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] - root_lin_vel_b = asset.data.root_lin_vel_b + root_lin_vel_b = asset.data.root_lin_vel_b.torch forward_velocity = root_lin_vel_b * torch.tensor(forward_vector, device=env.device, dtype=root_lin_vel_b.dtype) forward_reward = torch.sum(forward_velocity, dim=1) current_iter = int(env.common_step_counter / 48) @@ -311,14 +318,14 @@ def air_time_reward( if contact_sensor.cfg.track_air_time is False: raise RuntimeError("Activate ContactSensor's track_air_time!") # compute the reward - current_air_time = contact_sensor.data.current_air_time[:, sensor_cfg.body_ids] - current_contact_time = contact_sensor.data.current_contact_time[:, sensor_cfg.body_ids] + current_air_time = contact_sensor.data.current_air_time.torch[:, sensor_cfg.body_ids] + current_contact_time = contact_sensor.data.current_contact_time.torch[:, sensor_cfg.body_ids] t_max = torch.max(current_air_time, current_contact_time) t_min = torch.clip(t_max, max=mode_time) stance_cmd_reward = torch.clip(current_contact_time - current_air_time, -mode_time, mode_time) # cmd = torch.norm(env.command_manager.get_command("base_velocity"), dim=1).unsqueeze(dim=1).expand(-1, 4) - body_vel = torch.linalg.norm(asset.data.root_com_lin_vel_b[:, :2], dim=1).unsqueeze(dim=1).expand(-1, 4) + body_vel = torch.linalg.norm(asset.data.root_com_lin_vel_b.torch[:, :2], dim=1).unsqueeze(dim=1).expand(-1, 4) distance = torch.norm(env.command_manager.get_command("target_cmd")[:, :2], dim=1) reward = torch.where( (distance > 0.4) & (body_vel > velocity_threshold), @@ -435,8 +442,8 @@ def joint_position_penalty( """Penalize joint position error from default on the articulation.""" asset: Articulation = env.scene[asset_cfg.name] distance = torch.norm(env.command_manager.get_command("target_cmd")[:, :2], dim=1) - body_vel = torch.linalg.norm(asset.data.root_lin_vel_b[:, :2], dim=1) - reward = torch.linalg.norm((asset.data.joint_pos - asset.data.default_joint_pos), dim=1) + body_vel = torch.linalg.norm(asset.data.root_lin_vel_b.torch[:, :2], dim=1) + reward = torch.linalg.norm((asset.data.joint_pos.torch - asset.data.default_joint_pos.torch), dim=1) return torch.where( torch.logical_or(distance > 0.4, body_vel > velocity_threshold), reward, stand_still_scale * reward ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_beams_env.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_beams_env.py index 306faa3d..ccade4b5 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_beams_env.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_beams_env.py @@ -7,7 +7,7 @@ from isaaclab.managers import EventTermCfg as EventTerm from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.utils import configclass -from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise +from isaaclab.utils.noise import UniformNoiseCfg as Unoise import uwlab_tasks.manager_based.locomotion.risky_terrains.mdp as mdp diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_stones_env.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_stones_env.py index 55afb52a..a7f41976 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_stones_env.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_stones_env.py @@ -20,7 +20,8 @@ from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR -from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise +from isaaclab.utils.noise import UniformNoiseCfg as Unoise +from isaaclab_physx.physics import PhysxCfg import uwlab_tasks.manager_based.locomotion.risky_terrains.mdp as mdp @@ -336,10 +337,12 @@ def __post_init__(self): self.sim.render_interval = self.decimation self.sim.disable_contact_processing = True self.sim.physics_material = self.scene.terrain.physics_material - self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**24 - self.sim.physx.gpu_found_lost_pairs_capacity = 2**24 - self.sim.physx.gpu_collision_stack_size = 2**27 - self.sim.physx.gpu_max_rigid_patch_count = 6 * 2**15 + if self.sim.physics is None: + self.sim.physics = PhysxCfg() + self.sim.physics.gpu_total_aggregate_pairs_capacity = 2**24 + self.sim.physics.gpu_found_lost_pairs_capacity = 2**24 + self.sim.physics.gpu_collision_stack_size = 2**27 + self.sim.physics.gpu_max_rigid_patch_count = 6 * 2**15 self.viewer.resolution = (1920, 1080) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py index 52ca103c..00ae6241 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py +++ b/source/uwlab_tasks/uwlab_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="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": flat_env_cfg.AnymalCRoughEnvCfg, + "env_cfg_entry_point": f"{__name__}.flat_env_cfg:AnymalCRoughEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:AnymalCRoughPPORunnerFlatCfg", "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="isaaclab.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_cfg:AnymalCRoughPPORunnerFlatCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml", @@ -41,7 +41,7 @@ entry_point="isaaclab.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_cfg:AnymalCRoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", @@ -53,7 +53,7 @@ entry_point="isaaclab.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_cfg:AnymalCRoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml", diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py index eefaa1f1..b302d080 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py @@ -16,7 +16,7 @@ from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR -from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise +from isaaclab.utils.noise import UniformNoiseCfg as Unoise from isaaclab_tasks.manager_based.locomotion.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg ## diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/rough_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/rough_env_cfg.py index 1eeac6cb..8be52f86 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/rough_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/rough_env_cfg.py @@ -12,7 +12,7 @@ from isaaclab.managers import RewardTermCfg, SceneEntityCfg from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.utils import configclass -from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise +from isaaclab.utils.noise import UniformNoiseCfg as Unoise from isaaclab_tasks.manager_based.locomotion.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg ## diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/flat_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/flat_env_cfg.py index 3a8de5c5..a230c38a 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/flat_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/flat_env_cfg.py @@ -16,7 +16,7 @@ from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR -from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise +from isaaclab.utils.noise import UniformNoiseCfg as Unoise from isaaclab_tasks.manager_based.locomotion.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg from uwlab.envs.mdp import DefaultJointPositionStaticActionCfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/rough_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/rough_env_cfg.py index 5a92ae5f..fadf78f6 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/rough_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/rough_env_cfg.py @@ -12,7 +12,7 @@ from isaaclab.managers import RewardTermCfg, SceneEntityCfg from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.utils import configclass -from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise +from isaaclab.utils.noise import UniformNoiseCfg as Unoise from isaaclab_tasks.manager_based.locomotion.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg from uwlab.envs.mdp import DefaultJointPositionStaticActionCfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/curriculums.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/curriculums.py index 3bb0d1d5..171201b7 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/curriculums.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/curriculums.py @@ -43,7 +43,7 @@ def terrain_levels_vel( terrain: TerrainImporter = env.scene.terrain command = env.command_manager.get_command("base_velocity") # compute the distance the robot walked - distance = torch.norm(asset.data.root_pos_w[env_ids, :2] - env.scene.env_origins[env_ids, :2], dim=1) + distance = torch.norm(asset.data.root_pos_w.torch[env_ids, :2] - env.scene.env_origins[env_ids, :2], dim=1) # robots that walked far enough progress to harder terrains move_up = distance > terrain.cfg.terrain_generator.size[0] / 2 # robots that walked less than half of their required distance go to simpler terrains diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/rewards.py index 4baa9347..0fc64e73 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/rewards.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/rewards.py @@ -31,7 +31,7 @@ def feet_air_time( contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] # compute the reward first_contact = contact_sensor.compute_first_contact(env.step_dt)[:, sensor_cfg.body_ids] - last_air_time = contact_sensor.data.last_air_time[:, sensor_cfg.body_ids] + last_air_time = contact_sensor.data.last_air_time.torch[:, sensor_cfg.body_ids] reward = torch.sum((last_air_time - threshold) * first_contact, dim=1) # no reward for zero command reward *= torch.norm(env.command_manager.get_command(command_name)[:, :2], dim=1) > 0.1 @@ -48,8 +48,8 @@ def feet_air_time_positive_biped(env, command_name: str, threshold: float, senso """ contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] # compute the reward - air_time = contact_sensor.data.current_air_time[:, sensor_cfg.body_ids] - contact_time = contact_sensor.data.current_contact_time[:, sensor_cfg.body_ids] + air_time = contact_sensor.data.current_air_time.torch[:, sensor_cfg.body_ids] + contact_time = contact_sensor.data.current_contact_time.torch[:, sensor_cfg.body_ids] in_contact = contact_time > 0.0 in_mode_time = torch.where(in_contact, contact_time, air_time) single_stance = torch.sum(in_contact.int(), dim=1) == 1 @@ -63,9 +63,11 @@ def feet_air_time_positive_biped(env, command_name: str, threshold: float, senso def feet_slide(env, sensor_cfg: SceneEntityCfg, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: # Penalize feet sliding 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 + contacts = ( + contact_sensor.data.net_forces_w_history.torch[:, :, sensor_cfg.body_ids, :].norm(dim=-1).max(dim=1)[0] > 1.0 + ) asset = env.scene[asset_cfg.name] - body_vel = asset.data.body_lin_vel_w[:, asset_cfg.body_ids, :2] + body_vel = asset.data.body_lin_vel_w.torch[:, asset_cfg.body_ids, :2] reward = torch.sum(body_vel.norm(dim=-1) * contacts, dim=1) return reward @@ -76,7 +78,7 @@ def track_lin_vel_xy_yaw_frame_exp( """Reward tracking of linear velocity commands (xy axes) in the gravity aligned robot frame using exponential kernel.""" # extract the used quantities (to enable type-hinting) asset = env.scene[asset_cfg.name] - vel_yaw = quat_apply_inverse(yaw_quat(asset.data.root_quat_w), asset.data.root_lin_vel_w[:, :3]) + vel_yaw = quat_apply_inverse(yaw_quat(asset.data.root_quat_w.torch), asset.data.root_lin_vel_w.torch[:, :3]) lin_vel_error = torch.sum( torch.square(env.command_manager.get_command(command_name)[:, :2] - vel_yaw[:, :2]), dim=1 ) @@ -89,5 +91,7 @@ def track_ang_vel_z_world_exp( """Reward tracking of angular velocity commands (yaw) in world frame using exponential kernel.""" # extract the used quantities (to enable type-hinting) asset = env.scene[asset_cfg.name] - ang_vel_error = torch.square(env.command_manager.get_command(command_name)[:, 2] - asset.data.root_ang_vel_w[:, 2]) + ang_vel_error = torch.square( + env.command_manager.get_command(command_name)[:, 2] - asset.data.root_ang_vel_w.torch[:, 2] + ) return torch.exp(-ang_vel_error / std**2) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py index dda7d399..48d5e246 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py @@ -22,7 +22,8 @@ from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR -from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise +from isaaclab.utils.noise import UniformNoiseCfg as Unoise +from isaaclab_physx.physics import PhysxCfg from uwlab.terrains.config.rough import ROUGH_TERRAINS_CFG # isort: skip @@ -294,8 +295,10 @@ def __post_init__(self): self.sim.render_interval = self.decimation self.sim.disable_contact_processing = True self.sim.physics_material = self.scene.terrain.physics_material - self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**24 - self.sim.physx.gpu_found_lost_pairs_capacity = 2**24 + if self.sim.physics is None: + self.sim.physics = PhysxCfg() + self.sim.physics.gpu_total_aggregate_pairs_capacity = 2**24 + self.sim.physics.gpu_found_lost_pairs_capacity = 2**24 self.viewer.eye = (0.0, 0.0, 80) self.viewer.resolution = (1920, 1080) # update sensor update periods diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/assembly_keypoints.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/assembly_keypoints.py index a3cd5030..78890647 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/assembly_keypoints.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/assembly_keypoints.py @@ -18,17 +18,17 @@ @configclass class Offset: pos: tuple[float, float, float] = (0.0, 0.0, 0.0) - quat: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) + quat: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0) @property def pose(self) -> tuple[float, float, float, float, float, float, float]: return self.pos + self.quat def apply(self, root: RigidObject | Articulation) -> tuple[torch.Tensor, torch.Tensor]: - data = root.data.root_pos_w + data = root.data.root_pos_w.torch pos_w, quat_w = math_utils.combine_frame_transforms( - root.data.root_pos_w, - root.data.root_quat_w, + root.data.root_pos_w.torch, + root.data.root_quat_w.torch, torch.tensor(self.pos).to(data.device).repeat(data.shape[0], 1), torch.tensor(self.quat).to(data.device).repeat(data.shape[0], 1), ) @@ -49,10 +49,10 @@ def combine(self, pos: torch.Tensor, quat: torch.Tensor) -> tuple[torch.Tensor, class KeyPointsNistBoard: bolt_m16: Offset = Offset(pos=(0.145, -0.1495, -0.01)) hole_8mm: Offset = Offset(pos=(-0.0895, 0.0, 0.01)) - gear_base: Offset = Offset(pos=(0.145, 0.021, 0.0092), quat=(0.70711, 0.0, 0.0, -0.70711)) - small_gear: Offset = Offset(pos=(0.145, 0.021, 0.0092), quat=(0.70711, 0.0, 0.0, -0.70711)) - medium_gear: Offset = Offset(pos=(0.145, 0.021, 0.0092), quat=(0.70711, 0.0, 0.0, -0.70711)) - large_gear: Offset = Offset(pos=(0.145, 0.021, 0.0092), quat=(0.70711, 0.0, 0.0, -0.70711)) + gear_base: Offset = Offset(pos=(0.145, 0.021, 0.0092), quat=(0.0, 0.0, -0.70711, 0.70711)) + small_gear: Offset = Offset(pos=(0.145, 0.021, 0.0092), quat=(0.0, 0.0, -0.70711, 0.70711)) + medium_gear: Offset = Offset(pos=(0.145, 0.021, 0.0092), quat=(0.0, 0.0, -0.70711, 0.70711)) + large_gear: Offset = Offset(pos=(0.145, 0.021, 0.0092), quat=(0.0, 0.0, -0.70711, 0.70711)) @configclass @@ -67,7 +67,7 @@ class KeyPointsNutM16: center_axis_bottom: Offset = Offset(pos=(0.0, 0.0, 0.01)) center_axis_middle: Offset = Offset(pos=(0.0, 0.0, 0.0165)) center_axis_top: Offset = Offset(pos=(0.0, 0.0, 0.023)) - grasp_point: Offset = Offset(pos=(0.0, 0.0, 0.01), quat=(0.70711, 0.0, 0.0, -0.70711)) + grasp_point: Offset = Offset(pos=(0.0, 0.0, 0.01), quat=(0.0, 0.0, -0.70711, 0.70711)) grasp_diameter: float = 0.024 @@ -93,7 +93,7 @@ class KeyPointsSmallGear: class KeyPointsMediumGear: center_axis_bottom: Offset = Offset(pos=(0.02025, 0.0, 0.005)) center_axis_top: Offset = Offset(pos=(0.02025, 0.0, 0.03)) - grasp_point: Offset = Offset(pos=(0.02025, 0.0, 0.022), quat=(0.70711, 0.0, 0.0, -0.70711)) + grasp_point: Offset = Offset(pos=(0.02025, 0.0, 0.022), quat=(0.0, 0.0, -0.70711, 0.70711)) grasp_diameter: float = 0.03 @@ -122,7 +122,7 @@ class KeyPointsPeg8MM: @configclass class KeyPointPandaHand: - object_grasped_point: Offset = Offset(pos=(0.0, 0.0, 0.107), quat=(0.0, 0.0, 1.0, 0.0)) + object_grasped_point: Offset = Offset(pos=(0.0, 0.0, 0.107), quat=(0.0, 1.0, 0.0, 0.0)) KEYPOINTS_NISTBOARD = KeyPointsNistBoard() diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_assets_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_assets_cfg.py index 66d9fd8f..c0346743 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_assets_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_assets_cfg.py @@ -63,7 +63,7 @@ "panda_finger_joint2": 0.04, }, pos=(0.0, 0.0, 0.0), - rot=(1.0, 0.0, 0.0, 0.0), + rot=(0.0, 0.0, 0.0, 1.0), ), # Stiffness and dampness of the panda arm parts # will be set @@ -106,7 +106,7 @@ usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Mounts/UWPatVention/pat_vention.usd", rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, 0.0, -0.868), rot=(0.70711, 0.0, 0.0, -0.70711)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, 0.0, -0.868), rot=(0.0, 0.0, -0.70711, 0.70711)), ) # NIST Board @@ -145,7 +145,7 @@ collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), ), init_state=ArticulationCfg.InitialStateCfg( - pos=(0.55, 0.0, 0.05), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + pos=(0.55, 0.0, 0.05), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={}, joint_vel={} ), actuators={}, ) @@ -171,7 +171,7 @@ collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), ), init_state=ArticulationCfg.InitialStateCfg( - pos=(0.4, 0.3, 0.0), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + pos=(0.4, 0.3, 0.0), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={}, joint_vel={} ), actuators={}, ) @@ -198,7 +198,7 @@ collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), ), init_state=ArticulationCfg.InitialStateCfg( - pos=(0.6, 0.0, 0.05), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + pos=(0.6, 0.0, 0.05), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={}, joint_vel={} ), actuators={}, ) @@ -225,7 +225,7 @@ collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), ), init_state=ArticulationCfg.InitialStateCfg( - pos=(0.4, 0.35, 0.0), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + pos=(0.4, 0.35, 0.0), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={}, joint_vel={} ), actuators={}, ) @@ -251,7 +251,7 @@ collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), ), init_state=ArticulationCfg.InitialStateCfg( - pos=(0.0, 0.4, 0.1), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + pos=(0.0, 0.4, 0.1), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={}, joint_vel={} ), actuators={}, ) @@ -278,7 +278,7 @@ collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), ), init_state=ArticulationCfg.InitialStateCfg( - pos=(0.0, 0.45, 0.1), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + pos=(0.0, 0.45, 0.1), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={}, joint_vel={} ), actuators={}, ) @@ -305,7 +305,7 @@ collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), ), init_state=ArticulationCfg.InitialStateCfg( - pos=(0.65, 0.0, 0.05), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + pos=(0.65, 0.0, 0.05), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={}, joint_vel={} ), actuators={}, ) @@ -332,7 +332,7 @@ collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), ), init_state=ArticulationCfg.InitialStateCfg( - pos=(0.4, 0.40, 0.0), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + pos=(0.4, 0.40, 0.0), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={}, joint_vel={} ), actuators={}, ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_env_base.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_env_base.py index 4c3ebc0d..d3be029a 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_env_base.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_env_base.py @@ -15,6 +15,7 @@ from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.scene import InteractiveSceneCfg from isaaclab.utils import configclass +from isaaclab_physx.physics import PhysxCfg import uwlab_tasks.manager_based.manipulation.factory_extension.mdp as mdp @@ -304,16 +305,18 @@ def __post_init__(self) -> None: self.sim.dt = 1 / 120 self.sim.render_interval = self.decimation - self.sim.physx.solver_type = 1 - self.sim.physx.max_position_iteration_count = 192 # Important to avoid interpenetration. - self.sim.physx.max_velocity_iteration_count = 1 - self.sim.physx.bounce_threshold_velocity = 0.2 - self.sim.physx.friction_offset_threshold = 0.01 - self.sim.physx.friction_correlation_distance = 0.00625 - self.sim.physx.gpu_max_rigid_contact_count = 2**23 - self.sim.physx.gpu_max_rigid_patch_count = 2**23 - self.sim.physx.gpu_collision_stack_size = 2**31 - self.sim.physx.gpu_max_num_partitions = 1 + if self.sim.physics is None: + self.sim.physics = PhysxCfg() + self.sim.physics.solver_type = 1 + self.sim.physics.max_position_iteration_count = 192 # Important to avoid interpenetration. + self.sim.physics.max_velocity_iteration_count = 1 + self.sim.physics.bounce_threshold_velocity = 0.2 + self.sim.physics.friction_offset_threshold = 0.01 + self.sim.physics.friction_correlation_distance = 0.00625 + self.sim.physics.gpu_max_rigid_contact_count = 2**23 + self.sim.physics.gpu_max_rigid_patch_count = 2**23 + self.sim.physics.gpu_collision_stack_size = 2**31 + self.sim.physics.gpu_max_num_partitions = 1 self.sim.physics_material.static_friction = 1.0 self.sim.physics_material.dynamic_friction = 1.0 diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/actions/task_space_actions_nist.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/actions/task_space_actions_nist.py index 990d1799..0295c0bc 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/actions/task_space_actions_nist.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/actions/task_space_actions_nist.py @@ -140,7 +140,7 @@ def processed_actions(self) -> torch.Tensor: @property def jacobian_w(self) -> torch.Tensor: - return self._asset.root_physx_view.get_jacobians()[:, self._jacobi_body_idx, :, self._jacobi_joint_ids] + return self._asset.data.body_link_jacobian_w.torch[:, self._jacobi_body_idx, :, self._jacobi_joint_ids] @property def jacobian_b(self) -> torch.Tensor: @@ -200,8 +200,8 @@ def process_actions(self, actions: torch.Tensor): # set some useful reference to environment assets states fixed_asset: Articulation = self._env.scene["fixed_asset"] - fixed_pos = fixed_asset.data.root_link_pos_w - self._env.scene.env_origins - fixed_quat = fixed_asset.data.root_link_quat_w + fixed_pos = fixed_asset.data.root_link_pos_w.torch - self._env.scene.env_origins + fixed_quat = fixed_asset.data.root_link_quat_w.torch fixed_tip_pos_local = torch.zeros_like(fixed_pos) # here we want to be able to get the @@ -313,7 +313,7 @@ def _apply_task_space_gains(): # # adapted from https://gitlab-master.nvidia.com/carbon-gym/carbgym/-/blob/b4bbc66f4e31b1a1bee61dbaafc0766bbfbf0f58/python/examples/franka_cube_ik_osc.py#L70-78 # # roboticsproceedings.org/rss07/p31.pdf - arm_mass_matrix = self._asset.root_physx_view.get_generalized_mass_matrices()[:, 0:7, 0:7] + arm_mass_matrix = self._asset.data.mass_matrix.torch[:, 0:7, 0:7] # useful tensors arm_mass_matrix_inv = torch.inverse(arm_mass_matrix) arm_mass_matrix_task = torch.inverse( @@ -357,8 +357,8 @@ def reset(self, env_ids: Sequence[int] | None = None) -> None: # Custom part from NIST assembly - reset fixed_asset: Articulation = self._env.scene["fixed_asset"] robot: Articulation = self._env.scene["robot"] - fixed_pos = fixed_asset.data.root_link_pos_w - self._env.scene.env_origins - fixed_quat = fixed_asset.data.root_link_quat_w + fixed_pos = fixed_asset.data.root_link_pos_w.torch - self._env.scene.env_origins + fixed_quat = fixed_asset.data.root_link_quat_w.torch fixed_tip_pos_local = torch.zeros_like(fixed_pos) fixed_tip_pos_local[:, 2] += self.cfg.fixed_asset_cfg.height + self.cfg.fixed_asset_cfg.base_height @@ -369,9 +369,9 @@ def reset(self, env_ids: Sequence[int] | None = None) -> None: fixed_asset_pos_noise = fixed_asset_pos_noise @ torch.diag(fixed_asset_pos_rand) self.init_fixed_pos_obs_noise = fixed_asset_pos_noise fixed_pos_action_frame = fixed_tip_pos + self.init_fixed_pos_obs_noise - fingertip_midpoint_quat = robot.data.body_link_quat_w[:, self._body_idx] + fingertip_midpoint_quat = robot.data.body_link_quat_w.torch[:, self._body_idx] - fingertip_midpoint_pos = robot.data.body_link_pos_w[:, self._body_idx] - self._env.scene.env_origins + fingertip_midpoint_pos = robot.data.body_link_pos_w.torch[:, self._body_idx] - self._env.scene.env_origins pos_actions = fingertip_midpoint_pos - fixed_pos_action_frame pos_action_bounds = torch.tensor(CtrlCfg.pos_action_bounds, device=self.device) @@ -398,7 +398,7 @@ def reset(self, env_ids: Sequence[int] | None = None) -> None: yaw_action = (fingertip_yaw_fixed_asset + np.deg2rad(180.0)) / np.deg2rad(270.0) * 2.0 - 1.0 self._raw_actions[:, 5] = yaw_action - self._target_joint_pos_at_reset = robot.data.joint_pos_target.clone() + self._target_joint_pos_at_reset = robot.data.joint_pos_target.torch.clone() """ Helper functions. diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/events.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/events.py index e08a4fad..3f3353ca 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/events.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/events.py @@ -38,7 +38,7 @@ def reset_fixed_assets(env: ManagerBasedRLEnv, env_ids: torch.tensor, asset_list asset_on_board_pos, asset_on_board_quat = asset_offset_on_nist_board.apply(nistboard) root_pose = torch.cat((asset_on_board_pos, asset_on_board_quat), dim=1)[env_ids] asset.write_root_pose_to_sim(root_pose, env_ids=env_ids) - asset.write_root_velocity_to_sim(torch.zeros_like(asset.data.root_vel_w[env_ids]), env_ids=env_ids) + asset.write_root_velocity_to_sim(torch.zeros_like(asset.data.root_vel_w.torch[env_ids]), env_ids=env_ids) def reset_held_asset( @@ -52,8 +52,8 @@ def reset_held_asset( robot: Articulation = env.scene[holding_body_cfg.name] held_asset: Articulation = env.scene[held_asset_cfg.name] - end_effector_quat_w = robot.data.body_link_quat_w[env_ids, holding_body_cfg.body_ids].view(-1, 4) - end_effector_pos_w = robot.data.body_link_pos_w[env_ids, holding_body_cfg.body_ids].view(-1, 3) + end_effector_quat_w = robot.data.body_link_quat_w.torch[env_ids, holding_body_cfg.body_ids].view(-1, 4) + end_effector_pos_w = robot.data.body_link_pos_w.torch[env_ids, holding_body_cfg.body_ids].view(-1, 3) held_graspable_pos_b = torch.tensor(held_asset_graspable_offset.pos, device=env.device).repeat(len(env_ids), 1) held_graspable_quat_b = torch.tensor(held_asset_graspable_offset.quat, device=env.device).repeat(len(env_ids), 1) @@ -74,7 +74,7 @@ def reset_held_asset( new_quat_w = math_utils.quat_mul(translated_held_asset_quat, quat_b) held_asset.write_root_link_pose_to_sim(torch.cat([new_pos_w, new_quat_w], dim=1), env_ids=env_ids) # type: ignore - held_asset.write_root_com_velocity_to_sim(held_asset.data.default_root_state[env_ids, 7:], env_ids=env_ids) # type: ignore + held_asset.write_root_com_velocity_to_sim(held_asset.data.default_root_state.torch[env_ids, 7:], env_ids=env_ids) # type: ignore def grasp_held_asset( @@ -84,7 +84,7 @@ def grasp_held_asset( held_asset_diameter: float, ) -> None: robot: Articulation = env.scene[robot_cfg.name] - joint_pos = robot.data.joint_pos[:, robot_cfg.joint_ids][env_ids].clone() + joint_pos = robot.data.joint_pos.torch[:, robot_cfg.joint_ids][env_ids].clone() joint_pos[:, :] = held_asset_diameter / 2 * 1.25 robot.write_joint_state_to_sim(joint_pos, torch.zeros_like(joint_pos), robot_cfg.joint_ids, env_ids) # type: ignore diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/observations.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/observations.py index 97742bd9..76926301 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/observations.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/observations.py @@ -31,10 +31,10 @@ def target_asset_pose_in_root_asset_frame( taget_body_idx = 0 if isinstance(target_asset_cfg.body_ids, slice) else target_asset_cfg.body_ids root_body_idx = 0 if isinstance(root_asset_cfg.body_ids, slice) else root_asset_cfg.body_ids - target_pos = target_asset.data.body_link_pos_w[:, taget_body_idx].view(-1, 3) - target_quat = target_asset.data.body_link_quat_w[:, taget_body_idx].view(-1, 4) - root_pos = root_asset.data.body_link_pos_w[:, root_body_idx].view(-1, 3) - root_quat = root_asset.data.body_link_quat_w[:, root_body_idx].view(-1, 4) + target_pos = target_asset.data.body_link_pos_w.torch[:, taget_body_idx].view(-1, 3) + target_quat = target_asset.data.body_link_quat_w.torch[:, taget_body_idx].view(-1, 4) + root_pos = root_asset.data.body_link_pos_w.torch[:, root_body_idx].view(-1, 3) + root_quat = root_asset.data.body_link_quat_w.torch[:, root_body_idx].view(-1, 4) if root_asset_offset is not None: root_pos, root_quat = root_asset_offset.combine(root_pos, root_quat) @@ -55,15 +55,15 @@ def asset_link_velocity_in_root_asset_frame( target_body_idx = 0 if isinstance(target_asset_cfg.body_ids, slice) else target_asset_cfg.body_ids - asset_lin_vel_b, _ = math_utils.subtract_frame_transforms( - root_asset.data.root_pos_w, - root_asset.data.root_quat_w, - target_asset.data.body_lin_vel_w[:, target_body_idx].view(-1, 3), + root_quat_w = root_asset.data.root_quat_w.torch + + asset_lin_vel_b = math_utils.quat_apply_inverse( + root_quat_w, + target_asset.data.body_lin_vel_w.torch[:, target_body_idx].view(-1, 3), ) - asset_ang_vel_b, _ = math_utils.subtract_frame_transforms( - root_asset.data.root_pos_w, - root_asset.data.root_quat_w, - target_asset.data.body_ang_vel_w[:, target_body_idx].view(-1, 3), + asset_ang_vel_b = math_utils.quat_apply_inverse( + root_quat_w, + target_asset.data.body_ang_vel_w.torch[:, target_body_idx].view(-1, 3), ) return torch.cat([asset_lin_vel_b, asset_ang_vel_b], dim=1) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/assembly_keypoints.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/assembly_keypoints.py index d8c5e509..74fb352c 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/assembly_keypoints.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/assembly_keypoints.py @@ -18,17 +18,17 @@ @configclass class Offset: pos: tuple[float, float, float] = (0.0, 0.0, 0.0) - quat: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) + quat: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0) @property def pose(self) -> tuple[float, float, float, float, float, float, float]: return self.pos + self.quat def apply(self, root: RigidObject | Articulation) -> tuple[torch.Tensor, torch.Tensor]: - data = root.data.root_pos_w + data = root.data.root_pos_w.torch pos_w, quat_w = math_utils.combine_frame_transforms( - root.data.root_pos_w, - root.data.root_quat_w, + data, + root.data.root_quat_w.torch, torch.tensor(self.pos).to(data.device).repeat(data.shape[0], 1), torch.tensor(self.quat).to(data.device).repeat(data.shape[0], 1), ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/agents/rsl_rl_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/agents/rsl_rl_cfg.py index 8f6db98e..841fc6dc 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/agents/rsl_rl_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/agents/rsl_rl_cfg.py @@ -4,14 +4,9 @@ # SPDX-License-Identifier: BSD-3-Clause from isaaclab.utils import configclass -from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoAlgorithmCfg +from isaaclab_rl.rsl_rl import RslRlMLPModelCfg, RslRlOnPolicyRunnerCfg, RslRlPpoAlgorithmCfg -from uwlab_rl.rsl_rl.rl_cfg import ( - BehaviorCloningCfg, - OffPolicyAlgorithmCfg, - RslRlFancyActorCriticCfg, - RslRlFancyPpoAlgorithmCfg, -) +from uwlab_rl.rsl_rl.rl_cfg import BehaviorCloningCfg, OffPolicyAlgorithmCfg, RslRlFancyPpoAlgorithmCfg def my_experts_observation_func(env): @@ -24,17 +19,30 @@ class Base_PPORunnerCfg(RslRlOnPolicyRunnerCfg): num_steps_per_env = 32 max_iterations = 40000 save_interval = 100 + obs_groups = {"actor": ["policy"], "critic": ["policy"]} resume = False experiment_name = "ur5e_robotiq_2f85_omnireset_agent" - policy = RslRlFancyActorCriticCfg( - init_noise_std=1.0, - actor_obs_normalization=True, - critic_obs_normalization=True, - actor_hidden_dims=[512, 256, 128, 64], - critic_hidden_dims=[512, 256, 128, 64], + # Explicit `actor`/`critic` model cfgs preserve the distribution constructor options, + # including the action-std bounds not exposed by the legacy `policy` conversion. + actor = RslRlMLPModelCfg( + hidden_dims=[512, 256, 128, 64], activation="elu", - noise_std_type="gsde", - state_dependent_std=False, + obs_normalization=True, + # Bound normalized action std rather than feature-space noise weights. + # Predict state-dependent log std alongside the action mean while keeping + # the initial exploration scale identical to the previous recipe. + distribution_cfg={ + "class_name": "HeteroscedasticGaussianDistribution", + "init_std": 1.0, + "std_type": "log", + "std_range": [0.001, 2.0], + }, + ) + critic = RslRlMLPModelCfg( + hidden_dims=[512, 256, 128, 64], + activation="elu", + obs_normalization=True, + distribution_cfg=None, ) algorithm = RslRlPpoAlgorithmCfg( value_loss_coef=1.0, @@ -50,6 +58,10 @@ class Base_PPORunnerCfg(RslRlOnPolicyRunnerCfg): lam=0.95, desired_kl=0.01, max_grad_norm=1.0, + # Exploration is handled by the actor's distribution configuration. + # Gaussian noise is drawn independently for each action sample. + # There is no latent noise matrix to resample on environment steps. + # Leave the optimizer and rollout settings unchanged. ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/camera_align_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/camera_align_cfg.py index 6167c7b4..72996d3d 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/camera_align_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/camera_align_cfg.py @@ -32,6 +32,7 @@ from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.sensors import TiledCameraCfg from isaaclab.utils import configclass +from isaaclab_physx.physics import PhysxCfg from uwlab_assets.robots.ur5e_robotiq_gripper import EXPLICIT_UR5E_ROBOTIQ_2F85 @@ -58,7 +59,7 @@ class CameraAlignSceneCfg(RlStateSceneCfg): # --- Background curtains (match real workspace) --- curtain_left = RigidObjectCfg( prim_path="{ENV_REGEX_NS}/CurtainLeft", - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, -0.68, 0.519), rot=(0.707, 0.0, 0.0, -0.707)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, -0.68, 0.519), rot=(0.0, 0.0, -0.707, 0.707)), spawn=sim_utils.CuboidCfg( size=(0.01, 1.0, 1.125), rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), @@ -68,7 +69,7 @@ class CameraAlignSceneCfg(RlStateSceneCfg): ) curtain_back = RigidObjectCfg( prim_path="{ENV_REGEX_NS}/CurtainBack", - init_state=RigidObjectCfg.InitialStateCfg(pos=(-0.15, 0.0, 0.519), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(-0.15, 0.0, 0.519), rot=(0.0, 0.0, 0.0, 1.0)), spawn=sim_utils.CuboidCfg( size=(0.01, 1.3, 1.125), rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), @@ -78,7 +79,7 @@ class CameraAlignSceneCfg(RlStateSceneCfg): ) curtain_right = RigidObjectCfg( prim_path="{ENV_REGEX_NS}/CurtainRight", - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, 0.68, 0.519), rot=(0.707, 0.0, 0.0, -0.707)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, 0.68, 0.519), rot=(0.0, 0.0, -0.707, 0.707)), spawn=sim_utils.CuboidCfg( size=(0.01, 1.0, 1.125), rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), @@ -95,7 +96,7 @@ class CameraAlignSceneCfg(RlStateSceneCfg): width=640, offset=TiledCameraCfg.OffsetCfg( pos=(1.0770121, -0.1679045, 0.4486344), - rot=(0.70564552, 0.46613815, 0.25072644, 0.47107948), + rot=(0.46613815, 0.25072644, 0.47107948, 0.70564552), convention="opengl", ), data_types=["rgb"], @@ -109,7 +110,7 @@ class CameraAlignSceneCfg(RlStateSceneCfg): width=640, offset=TiledCameraCfg.OffsetCfg( pos=(0.8323904, 0.5877843, 0.2805111), - rot=(0.29008842, 0.22122445, 0.51336143, 0.77676798), + rot=(0.22122445, 0.51336143, 0.77676798, 0.29008842), convention="opengl", ), data_types=["rgb"], @@ -123,7 +124,7 @@ class CameraAlignSceneCfg(RlStateSceneCfg): width=640, offset=TiledCameraCfg.OffsetCfg( pos=(0.0182505, -0.00408447, -0.0689107), - rot=(0.34254336, -0.61819255, -0.6160212, 0.347879), + rot=(-0.61819255, -0.6160212, 0.347879, 0.34254336), convention="opengl", ), data_types=["rgb"], @@ -208,10 +209,13 @@ def __post_init__(self) -> None: self.scene.robot.init_state.pos = (0.0, -0.039, 0.0) self.scene.ur5_metal_support.init_state.pos = (0.0, -0.039, -0.013) + self.sim.use_newton_actuators = False + self.sim.physics = PhysxCfg(enable_external_forces_every_iteration=False) + # Render settings for visual fidelity - self.sim.render.enable_ambient_occlusion = True - self.sim.render.enable_reflections = True - self.sim.render.enable_dl_denoiser = True + task_mdp.configure_isaac_rtx( + self, enable_ambient_occlusion=True, enable_reflections=True, enable_dl_denoiser=True + ) self.sim.render_interval = 1 # rerender on reset diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/data_collection_rgb_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/data_collection_rgb_cfg.py index a5e06b11..b7dcb5d8 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/data_collection_rgb_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/data_collection_rgb_cfg.py @@ -15,7 +15,7 @@ from isaaclab.managers import ObservationTermCfg as ObsTerm from isaaclab.managers import SceneEntityCfg from isaaclab.managers import TerminationTermCfg as DoneTerm -from isaaclab.sensors import TiledCameraCfg +from isaaclab.sensors import JointWrenchSensorCfg, TiledCameraCfg from isaaclab.utils import configclass from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR @@ -27,10 +27,14 @@ @configclass class DataCollectionRGBObjectSceneCfg(RlStateSceneCfg): + # Incoming joint wrenches (binary_force_contact observation); Isaac Lab 3.0 moved these + # from ArticulationData to a sensor. + joint_wrench = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot") + # background curtain_left = RigidObjectCfg( prim_path="{ENV_REGEX_NS}/CurtainLeft", - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, -0.68, 0.519), rot=(0.707, 0.0, 0.0, -0.707)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, -0.68, 0.519), rot=(0.0, 0.0, -0.707, 0.707)), spawn=sim_utils.CuboidCfg( size=(0.01, 1.0, 1.125), rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), @@ -43,7 +47,7 @@ class DataCollectionRGBObjectSceneCfg(RlStateSceneCfg): curtain_back = RigidObjectCfg( prim_path="{ENV_REGEX_NS}/CurtainBack", - init_state=RigidObjectCfg.InitialStateCfg(pos=(-0.15, 0.0, 0.519), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(-0.15, 0.0, 0.519), rot=(0.0, 0.0, 0.0, 1.0)), spawn=sim_utils.CuboidCfg( size=(0.01, 1.3, 1.125), rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), @@ -56,7 +60,7 @@ class DataCollectionRGBObjectSceneCfg(RlStateSceneCfg): curtain_right = RigidObjectCfg( prim_path="{ENV_REGEX_NS}/CurtainRight", - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, 0.68, 0.519), rot=(0.707, 0.0, 0.0, -0.707)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, 0.68, 0.519), rot=(0.0, 0.0, -0.707, 0.707)), spawn=sim_utils.CuboidCfg( size=(0.01, 1.0, 1.125), rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), @@ -74,7 +78,7 @@ class DataCollectionRGBObjectSceneCfg(RlStateSceneCfg): width=320, offset=TiledCameraCfg.OffsetCfg( pos=(1.0770121, -0.1679045, 0.4486344), - rot=(0.70564552, 0.46613815, 0.25072644, 0.47107948), + rot=(0.46613815, 0.25072644, 0.47107948, 0.70564552), convention="opengl", ), data_types=["rgb"], @@ -88,7 +92,7 @@ class DataCollectionRGBObjectSceneCfg(RlStateSceneCfg): width=320, offset=TiledCameraCfg.OffsetCfg( pos=(0.8323904, 0.5877843, 0.2805111), - rot=(0.29008842, 0.22122445, 0.51336143, 0.77676798), + rot=(0.22122445, 0.51336143, 0.77676798, 0.29008842), convention="opengl", ), data_types=["rgb"], @@ -102,7 +106,7 @@ class DataCollectionRGBObjectSceneCfg(RlStateSceneCfg): width=320, offset=TiledCameraCfg.OffsetCfg( pos=(0.0182505, -0.00408447, -0.0689107), - rot=(0.34254336, -0.61819255, -0.6160212, 0.347879), + rot=(-0.61819255, -0.6160212, 0.347879, 0.34254336), convention="opengl", ), data_types=["rgb"], @@ -122,7 +126,7 @@ class BaseRGBEventCfg(FinetuneEvalEventCfg): "camera_path_template": "/World/envs/env_{}/Robot/rgb_front_camera", # Base values from TiledCameraCfg "base_position": (1.0770121, -0.1679045, 0.4486344), - "base_rotation": (0.70564552, 0.46613815, 0.25072644, 0.47107948), + "base_rotation": (0.46613815, 0.25072644, 0.47107948, 0.70564552), # Delta ranges for position (in meters) "position_deltas": {"x": (-0.05, 0.05), "y": (-0.05, 0.05), "z": (-0.05, 0.05)}, # Delta ranges for euler angles (in degrees) @@ -146,7 +150,7 @@ class BaseRGBEventCfg(FinetuneEvalEventCfg): "camera_path_template": "/World/envs/env_{}/Robot/rgb_side_camera", # Base values from TiledCameraCfg "base_position": (0.8323904, 0.5877843, 0.2805111), - "base_rotation": (0.29008842, 0.22122445, 0.51336143, 0.77676798), + "base_rotation": (0.22122445, 0.51336143, 0.77676798, 0.29008842), # Delta ranges for position (in meters) "position_deltas": {"x": (-0.05, 0.05), "y": (-0.05, 0.05), "z": (-0.05, 0.05)}, # Delta ranges for euler angles (in degrees) @@ -167,7 +171,7 @@ class BaseRGBEventCfg(FinetuneEvalEventCfg): "camera_path_template": "/World/envs/env_{}/Robot/robotiq_base_link/rgb_wrist_camera", # Base values from TiledCameraCfg "base_position": (0.0182505, -0.00408447, -0.0689107), - "base_rotation": (0.34254336, -0.61819255, -0.6160212, 0.347879), + "base_rotation": (-0.61819255, -0.6160212, 0.347879, 0.34254336), # Delta ranges for position (in meters) "position_deltas": {"x": (-0.01, 0.01), "y": (-0.01, 0.01), "z": (-0.01, 0.01)}, # Delta ranges for euler angles (in degrees) @@ -358,7 +362,7 @@ class RGBEventCfg(BaseRGBEventCfg): func=task_mdp.MultiResetManager, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "reset_types": ["ObjectAnywhereEEAnywhere"], "probs": [1.0], "success": "env.reward_manager.get_term_cfg('progress_context').func.success", @@ -374,7 +378,7 @@ class DataCollectionRGBEventCfg(RGBEventCfg): func=task_mdp.MultiResetManager, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "reset_types": [ "ObjectAnywhereEEAnywhere", "ObjectRestingEEGrasped", @@ -540,7 +544,7 @@ class RGBDataCollectionCfg(ObsGroup): binary_contact = ObsTerm( func=task_mdp.binary_force_contact, params={ - "asset_cfg": SceneEntityCfg("robot"), + "sensor_cfg": SceneEntityCfg("joint_wrench"), "body_name": "wrist_3_link", "force_threshold": 25.0, }, @@ -622,11 +626,14 @@ def __post_init__(self): self.episode_length_s = 32.0 # Render settings - self.sim.render.enable_dlssg = False - self.sim.render.enable_ambient_occlusion = True - self.sim.render.enable_reflections = True - self.sim.render.enable_dl_denoiser = True - self.sim.render.antialiasing_mode = "DLAA" + task_mdp.configure_isaac_rtx( + self, + enable_dlssg=False, + enable_ambient_occlusion=True, + enable_reflections=True, + enable_dl_denoiser=True, + antialiasing_mode="DLAA", + ) # speeds up rendering self.sim.render_interval = self.decimation @@ -790,7 +797,7 @@ class OODRGBEventCfg(BaseRGBEventCfg): func=task_mdp.MultiResetManager, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "reset_types": ["ObjectAnywhereEEAnywhere"], "probs": [1.0], "success": "env.reward_manager.get_term_cfg('progress_context').func.success", @@ -806,7 +813,7 @@ class DataCollectionOODRGBEventCfg(OODRGBEventCfg): func=task_mdp.MultiResetManager, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "reset_types": [ "ObjectAnywhereEEAnywhere", "ObjectRestingEEGrasped", diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/grasp_sampling_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/grasp_sampling_cfg.py index f7b112b5..5977ea30 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/grasp_sampling_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/grasp_sampling_cfg.py @@ -16,6 +16,7 @@ from isaaclab.scene import InteractiveSceneCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab_physx.physics import PhysxCfg import uwlab_assets.robots.ur5e_robotiq_gripper as ur5e_robotiq_gripper from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR @@ -39,10 +40,10 @@ class GraspSamplingSceneCfg(InteractiveSceneCfg): rigid_props=sim_utils.RigidBodyPropertiesCfg( solver_position_iteration_count=4, solver_velocity_iteration_count=0, disable_gravity=False ), - # assume very light - mass_props=sim_utils.MassPropertiesCfg(mass=0.001), + # Use the asset mass instead of assuming a very light object. + mass_props=None, ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT), rot=(0.0, 0.0, 0.0, 1.0)), ) # Environment @@ -161,9 +162,9 @@ def make_object(usd_path: str): disable_gravity=False, kinematic_enabled=False, ), - mass_props=sim_utils.MassPropertiesCfg(mass=0.001), + mass_props=None, ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 1.0), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 1.0), rot=(0.0, 0.0, 0.0, 1.0)), ) @@ -198,22 +199,29 @@ def __post_init__(self): # simulation settings self.sim.dt = 1 / 120.0 - # Contact and solver settings - self.sim.physx.solver_type = 1 - self.sim.physx.max_position_iteration_count = 192 - self.sim.physx.max_velocity_iteration_count = 1 - self.sim.physx.bounce_threshold_velocity = 0.02 - self.sim.physx.friction_offset_threshold = 0.01 - self.sim.physx.friction_correlation_distance = 0.0005 - - self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 - self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**23 - self.sim.physx.gpu_max_rigid_contact_count = 2**23 - self.sim.physx.gpu_max_rigid_patch_count = 2**23 - self.sim.physx.gpu_collision_stack_size = 2**31 + # Contact and solver settings, tuned for contact-rich peg insertion (note the + # 192 position iterations); these values back the sim-to-real transfer. + self.sim.physics = PhysxCfg( + solver_type=1, + enable_external_forces_every_iteration=False, + max_position_iteration_count=192, + max_velocity_iteration_count=1, + bounce_threshold_velocity=0.02, + friction_offset_threshold=0.01, + friction_correlation_distance=0.0005, + gpu_found_lost_aggregate_pairs_capacity=1024 * 1024 * 4, + gpu_total_aggregate_pairs_capacity=2**23, + gpu_max_rigid_contact_count=2**23, + gpu_max_rigid_patch_count=2**23, + gpu_collision_stack_size=2**31, + ) + self.sim.use_newton_actuators = False # Render settings - self.sim.render.enable_dlssg = True - self.sim.render.enable_ambient_occlusion = True - self.sim.render.enable_reflections = True - self.sim.render.enable_dl_denoiser = True + task_mdp.configure_isaac_rtx( + self, + enable_dlssg=True, + enable_ambient_occlusion=True, + enable_reflections=True, + enable_dl_denoiser=True, + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/partial_assemblies_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/partial_assemblies_cfg.py index b94ee380..04ba0a55 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/partial_assemblies_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/partial_assemblies_cfg.py @@ -15,6 +15,7 @@ from isaaclab.scene import InteractiveSceneCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab_physx.physics import PhysxCfg from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR @@ -41,7 +42,7 @@ class PartialAssembliesSceneCfg(InteractiveSceneCfg): # assume very light mass_props=sim_utils.MassPropertiesCfg(mass=0.001), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT * 2), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT * 2), rot=(0.0, 0.0, 0.0, 1.0)), ) receptive_object: RigidObjectCfg = RigidObjectCfg( @@ -58,7 +59,7 @@ class PartialAssembliesSceneCfg(InteractiveSceneCfg): # since kinematic_enabled=True, mass does not matter mass_props=sim_utils.MassPropertiesCfg(mass=0.5), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT), rot=(0.0, 0.0, 0.0, 1.0)), ) # Environment @@ -203,7 +204,7 @@ def make_insertive_object(usd_path: str): ), mass_props=sim_utils.MassPropertiesCfg(mass=0.001), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT * 2), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT * 2), rot=(0.0, 0.0, 0.0, 1.0)), ) @@ -221,7 +222,7 @@ def make_receptive_object(usd_path: str): ), mass_props=sim_utils.MassPropertiesCfg(mass=0.5), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT), rot=(0.0, 0.0, 0.0, 1.0)), ) @@ -268,22 +269,29 @@ def __post_init__(self): # simulation settings self.sim.dt = 1 / 120.0 - # Contact and solver settings - self.sim.physx.solver_type = 1 - self.sim.physx.max_position_iteration_count = 192 - self.sim.physx.max_velocity_iteration_count = 1 - self.sim.physx.bounce_threshold_velocity = 0.02 - self.sim.physx.friction_offset_threshold = 0.01 - self.sim.physx.friction_correlation_distance = 0.0005 - - self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 - self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**23 - self.sim.physx.gpu_max_rigid_contact_count = 2**23 - self.sim.physx.gpu_max_rigid_patch_count = 2**23 - self.sim.physx.gpu_collision_stack_size = 2**31 + # Contact and solver settings, tuned for contact-rich peg insertion (note the + # 192 position iterations); these values back the sim-to-real transfer. + self.sim.physics = PhysxCfg( + solver_type=1, + enable_external_forces_every_iteration=False, + max_position_iteration_count=192, + max_velocity_iteration_count=1, + bounce_threshold_velocity=0.02, + friction_offset_threshold=0.01, + friction_correlation_distance=0.0005, + gpu_found_lost_aggregate_pairs_capacity=1024 * 1024 * 4, + gpu_total_aggregate_pairs_capacity=2**23, + gpu_max_rigid_contact_count=2**23, + gpu_max_rigid_patch_count=2**23, + gpu_collision_stack_size=2**31, + ) + self.sim.use_newton_actuators = False # Render settings - self.sim.render.enable_dlssg = True - self.sim.render.enable_ambient_occlusion = True - self.sim.render.enable_reflections = True - self.sim.render.enable_dl_denoiser = True + task_mdp.configure_isaac_rtx( + self, + enable_dlssg=True, + enable_ambient_occlusion=True, + enable_reflections=True, + enable_dl_denoiser=True, + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/reset_states_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/reset_states_cfg.py index 85a042d2..f397ea96 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/reset_states_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/reset_states_cfg.py @@ -17,6 +17,7 @@ from isaaclab.scene import InteractiveSceneCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab_physx.physics import PhysxCfg from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR from uwlab_assets.robots.ur5e_robotiq_gripper import IMPLICIT_UR5E_ROBOTIQ_2F85 @@ -48,7 +49,7 @@ class ResetStatesSceneCfg(InteractiveSceneCfg): # assume very light mass_props=sim_utils.MassPropertiesCfg(mass=0.001), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(0.0, 0.0, 0.0, 1.0)), ) receptive_object: RigidObjectCfg = RigidObjectCfg( @@ -66,13 +67,13 @@ class ResetStatesSceneCfg(InteractiveSceneCfg): # since kinematic_enabled=True, mass does not matter mass_props=sim_utils.MassPropertiesCfg(mass=0.5), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(0.0, 0.0, 0.0, 1.0)), ) # Environment table = RigidObjectCfg( prim_path="{ENV_REGEX_NS}/Table", - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, 0.0, -0.881), rot=(0.707, 0.0, 0.0, -0.707)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, 0.0, -0.881), rot=(0.0, 0.0, -0.707, 0.707)), spawn=sim_utils.UsdFileCfg( usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Mounts/UWPatVention/pat_vention.usd", rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), @@ -81,7 +82,7 @@ class ResetStatesSceneCfg(InteractiveSceneCfg): ur5_metal_support = RigidObjectCfg( prim_path="{ENV_REGEX_NS}/UR5MetalSupport", - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0, -0.013), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0, -0.013), rot=(0.0, 0.0, 0.0, 1.0)), spawn=sim_utils.UsdFileCfg( usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Mounts/UWPatVention2/Ur5MetalSupport/ur5plate.usd", rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), @@ -236,7 +237,7 @@ class ObjectRestingEEGraspedEventCfg(ResetStatesBaseEventCfg): func=task_mdp.MultiResetManager, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "reset_types": ["ObjectAnywhereEEAnywhere"], "probs": [1.0], }, @@ -246,7 +247,7 @@ class ObjectRestingEEGraspedEventCfg(ResetStatesBaseEventCfg): func=task_mdp.reset_end_effector_from_grasp_dataset, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "fixed_asset_cfg": SceneEntityCfg("insertive_object"), "robot_ik_cfg": SceneEntityCfg( "robot", joint_names=["shoulder.*", "elbow.*", "wrist.*"], body_names="robotiq_base_link" @@ -289,7 +290,7 @@ class ObjectAnywhereEEGraspedEventCfg(ResetStatesBaseEventCfg): func=task_mdp.reset_end_effector_from_grasp_dataset, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "fixed_asset_cfg": SceneEntityCfg("insertive_object"), "robot_ik_cfg": SceneEntityCfg( "robot", joint_names=["shoulder.*", "elbow.*", "wrist.*"], body_names="robotiq_base_link" @@ -313,7 +314,7 @@ class ObjectPartiallyAssembledEEAnywhereEventCfg(ResetStatesBaseEventCfg): func=task_mdp.reset_insertive_object_from_partial_assembly_dataset, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "insertive_object_cfg": SceneEntityCfg("insertive_object"), "receptive_object_cfg": SceneEntityCfg("receptive_object"), "pose_range_b": { @@ -354,7 +355,7 @@ class ObjectPartiallyAssembledEEGraspedEventCfg(ResetStatesBaseEventCfg): func=task_mdp.reset_insertive_object_from_partial_assembly_dataset, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "insertive_object_cfg": SceneEntityCfg("insertive_object"), "receptive_object_cfg": SceneEntityCfg("receptive_object"), "pose_range_b": { @@ -372,7 +373,7 @@ class ObjectPartiallyAssembledEEGraspedEventCfg(ResetStatesBaseEventCfg): func=task_mdp.reset_end_effector_from_grasp_dataset, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "fixed_asset_cfg": SceneEntityCfg("insertive_object"), "robot_ik_cfg": SceneEntityCfg( "robot", joint_names=["shoulder.*", "elbow.*", "wrist.*"], body_names="robotiq_base_link" @@ -464,7 +465,7 @@ def make_insertive_object(usd_path: str): ), mass_props=sim_utils.MassPropertiesCfg(mass=0.001), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(0.0, 0.0, 0.0, 1.0)), ) @@ -482,7 +483,7 @@ def make_receptive_object(usd_path: str): ), mass_props=sim_utils.MassPropertiesCfg(mass=0.5), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(0.0, 0.0, 0.0, 1.0)), ) @@ -529,25 +530,32 @@ def __post_init__(self): # simulation settings self.sim.dt = 1 / 120.0 - # Contact and solver settings - self.sim.physx.solver_type = 1 - self.sim.physx.max_position_iteration_count = 192 - self.sim.physx.max_velocity_iteration_count = 1 - self.sim.physx.bounce_threshold_velocity = 0.02 - self.sim.physx.friction_offset_threshold = 0.01 - self.sim.physx.friction_correlation_distance = 0.0005 - - self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 - self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**23 - self.sim.physx.gpu_max_rigid_contact_count = 2**23 - self.sim.physx.gpu_max_rigid_patch_count = 2**23 - self.sim.physx.gpu_collision_stack_size = 2**31 + # Contact and solver settings, tuned for contact-rich peg insertion (note the + # 192 position iterations); these values back the sim-to-real transfer. + self.sim.physics = PhysxCfg( + solver_type=1, + enable_external_forces_every_iteration=False, + max_position_iteration_count=192, + max_velocity_iteration_count=1, + bounce_threshold_velocity=0.02, + friction_offset_threshold=0.01, + friction_correlation_distance=0.0005, + gpu_found_lost_aggregate_pairs_capacity=1024 * 1024 * 4, + gpu_total_aggregate_pairs_capacity=2**23, + gpu_max_rigid_contact_count=2**23, + gpu_max_rigid_patch_count=2**23, + gpu_collision_stack_size=2**31, + ) + self.sim.use_newton_actuators = False # Render settings - self.sim.render.enable_dlssg = True - self.sim.render.enable_ambient_occlusion = True - self.sim.render.enable_reflections = True - self.sim.render.enable_dl_denoiser = True + task_mdp.configure_isaac_rtx( + self, + enable_dlssg=True, + enable_ambient_occlusion=True, + enable_reflections=True, + enable_dl_denoiser=True, + ) @configclass diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/rl_state_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/rl_state_cfg.py index e35809cb..6e2afe5f 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/rl_state_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/config/ur5e_robotiq_2f85/rl_state_cfg.py @@ -20,6 +20,7 @@ from isaaclab.scene import InteractiveSceneCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab_physx.physics import PhysxCfg from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR from uwlab_assets.robots.ur5e_robotiq_gripper import EXPLICIT_UR5E_ROBOTIQ_2F85, IMPLICIT_UR5E_ROBOTIQ_2F85 @@ -51,7 +52,7 @@ class RlStateSceneCfg(InteractiveSceneCfg): ), mass_props=sim_utils.MassPropertiesCfg(mass=0.02), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(0.0, 0.0, 0.0, 1.0)), ) receptive_object: RigidObjectCfg = RigidObjectCfg( @@ -67,13 +68,13 @@ class RlStateSceneCfg(InteractiveSceneCfg): ), mass_props=sim_utils.MassPropertiesCfg(mass=0.5), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(0.0, 0.0, 0.0, 1.0)), ) # Environment table = RigidObjectCfg( prim_path="{ENV_REGEX_NS}/Table", - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, 0.0, -0.881), rot=(0.707, 0.0, 0.0, -0.707)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, 0.0, -0.881), rot=(0.0, 0.0, -0.707, 0.707)), spawn=sim_utils.UsdFileCfg( usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Mounts/UWPatVention/pat_vention.usd", rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), @@ -82,7 +83,7 @@ class RlStateSceneCfg(InteractiveSceneCfg): ur5_metal_support = RigidObjectCfg( prim_path="{ENV_REGEX_NS}/UR5MetalSupport", - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0, -0.013), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0, -0.013), rot=(0.0, 0.0, 0.0, 1.0)), spawn=sim_utils.UsdFileCfg( usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Mounts/UWPatVention2/Ur5MetalSupport/ur5plate.usd", rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), @@ -106,13 +107,15 @@ class RlStateSceneCfg(InteractiveSceneCfg): @configclass class BaseEventCfg: - """Shared events: material/mass randomization, gripper gains, scene reset. + """Shared events: material/mass randomization, gripper gains and reset. Does NOT include arm sysid or OSC gain randomization -- those differ between finetune (curriculum-ramped) and eval (fixed) stages. See ``FinetuneEventCfg`` and ``FinetuneEvalEventCfg``. """ + robot_wrist_armature = None + # mode: startup (randomize dynamics) robot_material = EventTerm( func=task_mdp.randomize_rigid_body_material, # type: ignore @@ -239,7 +242,7 @@ class TrainEventCfg(BaseEventCfg): func=task_mdp.MultiResetManager, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "reset_types": [ "ObjectAnywhereEEAnywhere", "ObjectRestingEEGrasped", @@ -260,7 +263,7 @@ class TrainEvalEventCfg(BaseEventCfg): func=task_mdp.MultiResetManager, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "reset_types": ["ObjectAnywhereEEAnywhere"], "probs": [1.0], "success": "env.reward_manager.get_term_cfg('progress_context').func.success", @@ -304,7 +307,7 @@ class FinetuneEvalEventCfg(BaseEventCfg): func=task_mdp.MultiResetManager, mode="reset", params={ - "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset", + "dataset_dir": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/OmniReset_isaaclab3", "reset_types": ["ObjectAnywhereEEAnywhere"], "probs": [1.0], "success": "env.reward_manager.get_term_cfg('progress_context').func.success", @@ -412,7 +415,7 @@ class PolicyCfg(ObsGroup): def __post_init__(self): self.enable_corruption = True self.concatenate_terms = True - self.history_length = 5 + self.history_length = 0 @configclass class CriticCfg(ObsGroup): @@ -516,7 +519,7 @@ def __post_init__(self): # observation groups policy: PolicyCfg = PolicyCfg() - critic: CriticCfg = CriticCfg() + critic: CriticCfg | None = None @configclass @@ -627,7 +630,7 @@ def make_insertive_object(usd_path: str): ), mass_props=sim_utils.MassPropertiesCfg(mass=0.001), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(0.0, 0.0, 0.0, 1.0)), ) @@ -645,7 +648,7 @@ def make_receptive_object(usd_path: str): ), mass_props=sim_utils.MassPropertiesCfg(mass=0.5), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(0.0, 0.0, 0.0, 1.0)), ) @@ -692,25 +695,34 @@ def __post_init__(self): # simulation settings self.sim.dt = 1 / 120.0 - # Contact and solver settings - self.sim.physx.solver_type = 1 - self.sim.physx.max_position_iteration_count = 192 - self.sim.physx.max_velocity_iteration_count = 1 - self.sim.physx.bounce_threshold_velocity = 0.02 - self.sim.physx.friction_offset_threshold = 0.01 - self.sim.physx.friction_correlation_distance = 0.0005 + # Contact and solver settings, tuned for contact-rich peg insertion (note the 192 + # position iterations); these are what the sim-to-real transfer was validated + # against, so do not retune them casually. + self.sim.physics = PhysxCfg( + solver_type=1, + enable_external_forces_every_iteration=False, + max_position_iteration_count=192, + max_velocity_iteration_count=1, + bounce_threshold_velocity=0.02, + friction_offset_threshold=0.01, + friction_correlation_distance=0.0005, + gpu_found_lost_aggregate_pairs_capacity=1024 * 1024 * 4, + gpu_total_aggregate_pairs_capacity=2**23, + gpu_max_rigid_contact_count=2**23, + gpu_max_rigid_patch_count=2**23, + gpu_collision_stack_size=2**31, + ) - self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 - self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**23 - self.sim.physx.gpu_max_rigid_contact_count = 2**23 - self.sim.physx.gpu_max_rigid_patch_count = 2**23 - self.sim.physx.gpu_collision_stack_size = 2**31 + self.sim.use_newton_actuators = False # Render settings - self.sim.render.enable_dlssg = True - self.sim.render.enable_ambient_occlusion = True - self.sim.render.enable_reflections = True - self.sim.render.enable_dl_denoiser = True + task_mdp.configure_isaac_rtx( + self, + enable_dlssg=True, + enable_ambient_occlusion=True, + enable_reflections=True, + enable_dl_denoiser=True, + ) # Training configuration (Stage 1: no curriculum, implicit actuator, no sysid DR) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/actions/actions_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/actions/actions_cfg.py index 5a1de1c8..53e5d723 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/actions/actions_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/actions/actions_cfg.py @@ -34,8 +34,8 @@ class OffsetCfg: pos: tuple[float, float, float] = (0.0, 0.0, 0.0) """Translation offset.""" - rot: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) - """Rotation offset as quaternion (w, x, y, z).""" + rot: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0) + """Rotation offset as quaternion (x, y, z, w).""" joint_names: list[str] = MISSING """Joint names for the arm (regex supported).""" diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/actions/task_space_actions.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/actions/task_space_actions.py index 857610bb..607eccce 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/actions/task_space_actions.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/actions/task_space_actions.py @@ -132,7 +132,7 @@ def process_actions(self, actions: torch.Tensor): axis = delta_rot / safe_angle axis = torch.where(angle > 1e-6, axis, torch.zeros_like(axis)) half = angle / 2.0 - delta_quat = torch.cat([torch.cos(half), axis * torch.sin(half)], dim=-1) + delta_quat = torch.cat([axis * torch.sin(half), torch.cos(half)], dim=-1) self._ee_quat_des[:] = math_utils.quat_mul(delta_quat, ee_quat_b) def apply_actions(self): @@ -142,8 +142,8 @@ def apply_actions(self): """ # Current state ee_pos_b, ee_quat_b = self._get_ee_pose_root_frame() - joint_pos = self._asset.data.joint_pos[:, self._joint_ids] - joint_vel = self._asset.data.joint_vel[:, self._joint_ids] + joint_pos = self._asset.data.joint_pos.torch[:, self._joint_ids] + joint_vel = self._asset.data.joint_vel.torch[:, self._joint_ids] # Analytical Jacobian (base_link frame, matching EE pose frame) jacobian = compute_jacobian_analytical(joint_pos, device=str(self.device)) @@ -180,11 +180,11 @@ def reset(self, env_ids: Sequence[int] | None = None) -> None: def _get_ee_pose_root_frame(self) -> tuple[torch.Tensor, torch.Tensor]: """Get EE pose in root (base_link) frame from sim state.""" - ee_pos_w = self._asset.data.body_pos_w[:, self._ee_body_idx] - ee_quat_w = self._asset.data.body_quat_w[:, self._ee_body_idx] + ee_pos_w = self._asset.data.body_pos_w.torch[:, self._ee_body_idx] + ee_quat_w = self._asset.data.body_quat_w.torch[:, self._ee_body_idx] ee_pos_b, ee_quat_b = math_utils.subtract_frame_transforms( - self._asset.data.root_pos_w, - self._asset.data.root_quat_w, + self._asset.data.root_pos_w.torch, + self._asset.data.root_quat_w.torch, ee_pos_w, ee_quat_w, ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/collision_analyzer.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/collision_analyzer.py index 10a89e73..9fdf144c 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/collision_analyzer.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/collision_analyzer.py @@ -51,16 +51,17 @@ def __init__(self, cfg: CollisionAnalyzerCfg, env: ManagerBasedRLEnv): self.body_ids = [] self.local_pts = [] + template_path = RigidObjectHasher.resolve_prim_paths(env.num_envs, self.asset.cfg.prim_path)[0] for i, body_name in enumerate(body_names): # start = time.perf_counter() prim = get_first_matching_child_prim( - self.asset.cfg.prim_path.replace(".*", "0", 1), # we use the 0th env prim as template + template_path, # we use the 0th env prim as template predicate=lambda p: p.GetName() == body_name and p.HasAPI(UsdPhysics.RigidBodyAPI), ) local_pts = utils.sample_object_point_cloud( num_envs=env.num_envs, num_points=cfg.num_points, - prim_path_pattern=str(prim.GetPath()).replace("env_0", "env_.*", 1), + prim_path_pattern=self.asset.cfg.prim_path + str(prim.GetPath())[len(template_path) :], device=env.device, ) if local_pts is not None: @@ -116,12 +117,12 @@ def __init__(self, cfg: CollisionAnalyzerCfg, env: ManagerBasedRLEnv): def __call__(self, env: ManagerBasedRLEnv, env_ids: torch.Tensor): pos_w = ( - self.asset.data.body_link_pos_w[env_ids][:, self.body_ids] + self.asset.data.body_link_pos_w.torch[env_ids][:, self.body_ids] .unsqueeze(2) .expand(-1, -1, self.cfg.num_points, 3) ) quat_w = ( - self.asset.data.body_link_quat_w[env_ids][:, self.body_ids] + self.asset.data.body_link_quat_w.torch[env_ids][:, self.body_ids] .unsqueeze(2) .expand(-1, -1, self.cfg.num_points, 4) ) @@ -129,14 +130,16 @@ def __call__(self, env: ManagerBasedRLEnv, env_ids: torch.Tensor): obstacles_pos_w = torch.cat( [ - obstacle.data.root_pos_w[env_ids].view(-1, 1, 1, 3).expand(-1, -1, self.cfg.num_points, 3) + obstacle.data.root_pos_w.torch[env_ids].view(-1, 1, 1, 3).expand(-1, -1, self.cfg.num_points, 3) for obstacle in self.obstacles ], dim=0, ) obstacles_quat_w = torch.cat( [ - obstacle.data.root_quat_w[env_ids].view(-1, 1, 1, 4).expand(-1, cloud.shape[1], self.cfg.num_points, 4) + obstacle.data.root_quat_w.torch[env_ids] + .view(-1, 1, 1, 4) + .expand(-1, cloud.shape[1], self.cfg.num_points, 4) for obstacle in self.obstacles ], dim=0, diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/events.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/events.py index 0769039c..fd6581d1 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/events.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/events.py @@ -14,6 +14,7 @@ import trimesh import trimesh.transformations as tra from collections.abc import Sequence +from scipy.spatial.transform import Rotation as R import carb import isaaclab.sim as sim_utils @@ -21,12 +22,17 @@ import omni.usd from isaaclab.assets import Articulation, RigidObject from isaaclab.controllers import DifferentialIKControllerCfg -from isaaclab.envs import ManagerBasedEnv +from isaaclab.envs import ManagerBasedEnv, ManagerBasedEnvCfg from isaaclab.envs.mdp.actions.task_space_actions import DifferentialInverseKinematicsAction from isaaclab.managers import EventTermCfg, ManagerTermBase, SceneEntityCfg from isaaclab.markers import VisualizationMarkers from isaaclab.markers.config import FRAME_MARKER_CFG -from pxr import Gf, UsdGeom, UsdLux +from isaaclab.sensors import CameraCfg +from isaaclab_physx.renderers import IsaacRtxRendererCfg, IsaacRtxRendererGlobalSettingsCfg +from isaaclab_physx.renderers.isaac_rtx_renderer_utils import apply_isaac_rtx_global_settings +from pxr import Gf, Sdf, Usd, UsdGeom, UsdLux, UsdShade + +from uwlab_assets.robots.ur5e_robotiq_gripper.kinematics import ARM_JOINT_NAMES from uwlab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg @@ -36,6 +42,30 @@ from .success_monitor_cfg import SuccessMonitorCfg +def apply_isaac_rtx_settings( + env: ManagerBasedEnv, + env_ids: torch.Tensor | None, + settings: IsaacRtxRendererGlobalSettingsCfg, +): + """Apply process-global RTX settings during environment startup.""" + if settings.antialiasing_mode is not None: + sim_utils.enable_extension("omni.replicator.core") + apply_isaac_rtx_global_settings(settings) + + +def configure_isaac_rtx(env_cfg: ManagerBasedEnvCfg, **settings: bool | str): + """Preserve the task's renderer settings across the EA renderer API migration.""" + global_settings = IsaacRtxRendererGlobalSettingsCfg(**settings) + env_cfg.events.render_settings = EventTermCfg( + func=apply_isaac_rtx_settings, mode="startup", params={"settings": global_settings} + ) + for sensor_cfg in vars(env_cfg.scene).values(): + if isinstance(sensor_cfg, CameraCfg): + sensor_cfg.renderer_cfg = IsaacRtxRendererCfg( + enable_scene_partitioning=False, global_settings=global_settings.copy() + ) + + class grasp_sampling_event(ManagerTermBase): """EventTerm class for grasp sampling and positioning gripper.""" @@ -141,7 +171,7 @@ def _extract_mesh_from_asset(self, asset): stage = omni.usd.get_context().get_stage() # For multi-environment setups, we need to get the first environment's path - prim_path = asset.cfg.prim_path.replace(".*", "0", 1) + prim_path = utils.RigidObjectHasher.resolve_prim_paths(self._env.num_envs, asset.cfg.prim_path, stage=stage)[0] # Get the USD prim prim = stage.GetPrimAtPath(prim_path) @@ -157,8 +187,6 @@ def _find_mesh_in_prim(self, prim): if prim.IsA(UsdGeom.Mesh): return UsdGeom.Mesh(prim) - from pxr import Usd - for child in Usd.PrimRange(prim): if child.IsA(UsdGeom.Mesh): return UsdGeom.Mesh(child) @@ -326,8 +354,8 @@ def _apply_grasp_transform_to_gripper(self, env, gripper_asset, grasp_transform, """Apply grasp transform to gripper asset.""" # Get object's current pose in world coordinates object_asset = env.scene[self.object_cfg.name] - object_pos = object_asset.data.root_pos_w[env_idx] - object_quat = object_asset.data.root_quat_w[env_idx] + object_pos = object_asset.data.root_pos_w.torch[env_idx] + object_quat = object_asset.data.root_quat_w.torch[env_idx] # Convert numpy transform matrix to torch tensors (object-local coordinates) transform_tensor = torch.tensor(grasp_transform, dtype=torch.float32, device=env.device) @@ -341,20 +369,22 @@ def _apply_grasp_transform_to_gripper(self, env, gripper_asset, grasp_transform, ) # Apply world transform to gripper asset for the specific environment - gripper_asset.data.root_pos_w[env_idx] = world_pos[0] - gripper_asset.data.root_quat_w[env_idx] = world_quat[0] + gripper_asset.data.root_pos_w.torch[env_idx] = world_pos[0] + gripper_asset.data.root_quat_w.torch[env_idx] = world_quat[0] # Write the new pose to simulation indices = torch.tensor([env_idx], device=env.device) - root_pose = torch.cat([gripper_asset.data.root_pos_w[indices], gripper_asset.data.root_quat_w[indices]], dim=-1) + root_pose = torch.cat( + [gripper_asset.data.root_pos_w.torch[indices], gripper_asset.data.root_quat_w.torch[indices]], dim=-1 + ) gripper_asset.write_root_pose_to_sim(root_pose, env_ids=indices) def _apply_grasp_transforms_vectorized(self, env, gripper_asset, grasp_transforms, env_ids): """Apply grasp transforms to gripper assets for multiple environments (vectorized).""" # Get object's current pose in world coordinates for all environments object_asset = env.scene[self.object_cfg.name] - object_pos = object_asset.data.root_pos_w[env_ids] - object_quat = object_asset.data.root_quat_w[env_ids] + object_pos = object_asset.data.root_pos_w.torch[env_ids] + object_quat = object_asset.data.root_quat_w.torch[env_ids] # Extract positions and quaternions from transform matrices (already tensors) local_positions = grasp_transforms[:, :3, 3] # Extract translation @@ -367,8 +397,8 @@ def _apply_grasp_transforms_vectorized(self, env, gripper_asset, grasp_transform ) # Apply world transforms to gripper assets (vectorized) - gripper_asset.data.root_pos_w[env_ids] = world_positions - gripper_asset.data.root_quat_w[env_ids] = world_quaternions + gripper_asset.data.root_pos_w.torch[env_ids] = world_positions + gripper_asset.data.root_quat_w.torch[env_ids] = world_quaternions # Write the new poses to simulation (single vectorized call) root_poses = torch.cat([world_positions, world_quaternions], dim=-1) @@ -383,8 +413,8 @@ def _visualize_grasp_poses(self, env, scale: float = 0.03): object_asset = env.scene[self.object_cfg.name] # Get object's current pose in world coordinates - object_pos = object_asset.data.root_pos_w[0] # Use first environment - object_quat = object_asset.data.root_quat_w[0] # Use first environment + object_pos = object_asset.data.root_pos_w.torch[0] # Use first environment + object_quat = object_asset.data.root_quat_w.torch[0] # Use first environment # Convert grasp transforms to poses and transform to world coordinates world_positions = [] @@ -414,7 +444,7 @@ def _visualize_grasp_poses(self, env, scale: float = 0.03): def _open_gripper(self, env, gripper_asset, env_ids): """Open gripper to prepare for grasping.""" # Get current joint positions - current_joint_pos = gripper_asset.data.joint_pos[env_ids].clone() + current_joint_pos = gripper_asset.data.joint_pos.torch[env_ids].clone() # Find joint indices using configurable joint names and positions joint_configs = [] @@ -443,13 +473,13 @@ def _ensure_stable_gripper_state(self, env, gripper_asset, env_ids): gripper_asset.reset(env_ids) # 2. Reset to default root state (position and velocity) - default_root_state = gripper_asset.data.default_root_state[env_ids].clone() + default_root_state = gripper_asset.data.default_root_state.torch[env_ids].clone() default_root_state[:, 0:3] += env.scene.env_origins[env_ids] gripper_asset.write_root_state_to_sim(default_root_state, env_ids=env_ids) # 3. Reset all joints to default positions with zero velocities - default_joint_pos = gripper_asset.data.default_joint_pos[env_ids].clone() - zero_joint_vel = torch.zeros_like(gripper_asset.data.default_joint_vel[env_ids]) + default_joint_pos = gripper_asset.data.default_joint_pos.torch[env_ids].clone() + zero_joint_vel = torch.zeros_like(gripper_asset.data.default_joint_vel.torch[env_ids]) gripper_asset.write_joint_state_to_sim(default_joint_pos, zero_joint_vel, env_ids=env_ids) # 4. Set joint targets to default positions to prevent drift @@ -561,8 +591,8 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): scale=1.0, ) self.solver: DifferentialInverseKinematicsAction = robot_ik_solver_cfg.class_type(robot_ik_solver_cfg, env) # type: ignore - self.reset_velocity = torch.zeros((env.num_envs, self.robot.data.joint_vel.shape[1]), device=env.device) - self.reset_position = torch.zeros((env.num_envs, self.robot.data.joint_pos.shape[1]), device=env.device) + self.reset_velocity = torch.zeros((env.num_envs, self.robot.data.joint_vel.torch.shape[1]), device=env.device) + self.reset_position = torch.zeros((env.num_envs, self.robot.data.joint_pos.torch.shape[1]), device=env.device) def __call__( self, @@ -575,8 +605,8 @@ def __call__( ) -> None: if fixed_asset_offset is None: fixed_tip_pos_w, fixed_tip_quat_w = ( - env.scene[fixed_asset_cfg.name].data.root_pos_w, - env.scene[fixed_asset_cfg.name].data.root_quat_w, + env.scene[fixed_asset_cfg.name].data.root_pos_w.torch, + env.scene[fixed_asset_cfg.name].data.root_quat_w.torch, ) else: fixed_tip_pos_w, fixed_tip_quat_w = self.fixed_asset_offset.apply(self.fixed_asset) @@ -587,16 +617,18 @@ def __call__( pos_w = fixed_tip_pos_w + samples[:, 0:3] quat_w = math_utils.quat_from_euler_xyz(samples[:, 3], samples[:, 4], samples[:, 5]) pos_b, quat_b = math_utils.subtract_frame_transforms( - self.robot.data.root_link_pos_w, self.robot.data.root_link_quat_w, pos_w, quat_w + self.robot.data.root_link_pos_w.torch, self.robot.data.root_link_quat_w.torch, pos_w, quat_w ) self.solver.process_actions(torch.cat([pos_b, quat_b], dim=1)) # Error Rate 75% ^ 10 = 0.05 (final error) for i in range(10): self.solver.apply_actions() - delta_joint_pos = 0.25 * (self.robot.data.joint_pos_target[env_ids] - self.robot.data.joint_pos[env_ids]) + delta_joint_pos = 0.25 * ( + self.robot.data.joint_pos_target[env_ids] - self.robot.data.joint_pos.torch[env_ids] + ) self.robot.write_joint_state_to_sim( - position=(delta_joint_pos + self.robot.data.joint_pos[env_ids])[:, self.joint_ids], + position=(delta_joint_pos + self.robot.data.joint_pos.torch[env_ids])[:, self.joint_ids], velocity=torch.zeros((len(env_ids), self.n_joints), device=env.device), joint_ids=self.joint_ids, env_ids=env_ids, # type: ignore @@ -655,6 +687,7 @@ def _load_and_precompute_grasps(self, env): """Load Torch (.pt) grasp data and convert to optimized tensors.""" local_path = utils.safe_retrieve_file_path(self.grasp_dataset_path) data = torch.load(local_path, map_location="cpu") + _require_isaaclab3_dataset(data, self.grasp_dataset_path) # TorchDatasetFileHandler stores nested dicts; grasp data likely under 'grasp_relative_pose' grasp_group = data.get("grasp_relative_pose", data) @@ -719,8 +752,8 @@ def __call__( ) -> None: """Apply grasp poses to reset end effector.""" # RigidObject asset - object_pos_w = self.fixed_asset.data.root_pos_w[env_ids] - object_quat_w = self.fixed_asset.data.root_quat_w[env_ids] + object_pos_w = self.fixed_asset.data.root_pos_w.torch[env_ids] + object_quat_w = self.fixed_asset.data.root_quat_w.torch[env_ids] # Randomly sample grasp indices for each environment num_envs = len(env_ids) @@ -759,9 +792,11 @@ def __call__( # Solve IK iteratively for better convergence for i in range(25): self.solver.apply_actions() - delta_joint_pos = 0.25 * (self.robot.data.joint_pos_target[env_ids] - self.robot.data.joint_pos[env_ids]) + delta_joint_pos = 0.25 * ( + self.robot.data.joint_pos_target[env_ids] - self.robot.data.joint_pos.torch[env_ids] + ) self.robot.write_joint_state_to_sim( - position=(delta_joint_pos + self.robot.data.joint_pos[env_ids])[:, self.joint_ids], + position=(delta_joint_pos + self.robot.data.joint_pos.torch[env_ids])[:, self.joint_ids], velocity=torch.zeros((len(env_ids), self.n_joints), device=env.device), joint_ids=self.joint_ids, env_ids=env_ids, # type: ignore @@ -813,6 +848,7 @@ def _load_and_precompute_partial_assemblies(self, env): """Load Torch (.pt) partial assembly data and convert to optimized tensors.""" local_path = utils.safe_retrieve_file_path(self.partial_assembly_dataset_path) data = torch.load(local_path, map_location="cpu") + _require_isaaclab3_dataset(data, self.partial_assembly_dataset_path) rel_pos = data.get("relative_position") rel_quat = data.get("relative_orientation") @@ -845,8 +881,8 @@ def __call__( ) -> None: """Reset the insertive object from a partial assembly dataset.""" # Get receptive object pose (world coordinates) - receptive_pos_w = self.receptive_object.data.root_pos_w[env_ids] - receptive_quat_w = self.receptive_object.data.root_quat_w[env_ids] + receptive_pos_w = self.receptive_object.data.root_pos_w.torch[env_ids] + receptive_quat_w = self.receptive_object.data.root_quat_w.torch[env_ids] # Randomly sample partial assembly indices for each environment num_envs = len(env_ids) @@ -906,10 +942,10 @@ def __call__( """Collect pose data from all environments.""" # Get object poses for all environments - receptive_pos = self.receptive_object.data.root_pos_w[env_ids] - receptive_quat = self.receptive_object.data.root_quat_w[env_ids] - insertive_pos = self.insertive_object.data.root_pos_w[env_ids] - insertive_quat = self.insertive_object.data.root_quat_w[env_ids] + receptive_pos = self.receptive_object.data.root_pos_w.torch[env_ids] + receptive_quat = self.receptive_object.data.root_quat_w.torch[env_ids] + insertive_pos = self.insertive_object.data.root_pos_w.torch[env_ids] + insertive_quat = self.insertive_object.data.root_quat_w.torch[env_ids] # Calculate relative transform relative_pos, relative_quat = math_utils.subtract_frame_transforms( @@ -961,8 +997,8 @@ def __call__( """Spawn insertive object at assembled offset position.""" # Get receptive object poses - receptive_pos = self.receptive_object.data.root_pos_w[env_ids] - receptive_quat = self.receptive_object.data.root_quat_w[env_ids] + receptive_pos = self.receptive_object.data.root_pos_w.torch[env_ids] + receptive_quat = self.receptive_object.data.root_quat_w.torch[env_ids] # Apply receptive assembled offset to get target position target_pos, target_quat = self.receptive_assembled_offset.combine(receptive_pos, receptive_quat) @@ -992,6 +1028,21 @@ def __call__( ) +def _require_isaaclab3_dataset(data: dict, dataset_file: str) -> None: + """Refuse datasets that are not stamped with the Isaac Lab 3.0 quaternion convention. + + Datasets recorded under Isaac Lab 2.x hold ``(w, x, y, z)`` quaternions; loading them + silently rotates every reset pose by 180 degrees about X. The converted datasets are published + as ``Datasets/OmniReset_isaaclab3`` on the ``isaaclab3`` branch of the cloud asset repository. + """ + convention = data.pop("quat_convention", None) + if convention != "xyzw": + raise ValueError( + f"{dataset_file} is not an Isaac Lab 3.0 dataset (quat_convention={convention!r}, expected 'xyzw');" + " use Datasets/OmniReset_isaaclab3 from the isaaclab3 cloud asset branch" + ) + + class MultiResetManager(ManagerTermBase): def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): super().__init__(cfg, env) @@ -1026,6 +1077,7 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): raise FileNotFoundError(f"Dataset file {dataset_file} could not be accessed or downloaded.") dataset = torch.load(local_file_path) + _require_isaaclab3_dataset(dataset, dataset_file) num_states.append(len(dataset["initial_state"]["articulation"]["robot"]["joint_position"])) init_indices = torch.arange(num_states[-1], device=env.device) self.datasets.append(sample_state_data_set(dataset, init_indices, env.device)) @@ -1243,11 +1295,11 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): torch.tensor(bottom_offset.get("pos"), device=env.device).unsqueeze(0).repeat(env.num_envs, 1) ) assert tuple(bottom_offset.get("quat")) == ( - 1.0, 0.0, 0.0, 0.0, - ), "Bottom offset rotation must be (1.0, 0.0, 0.0, 0.0)" + 1.0, + ), "Bottom offset rotation must be identity" def __call__( self, @@ -1279,14 +1331,14 @@ def __call__( asset: RigidObject | Articulation = env.scene[asset_cfg.name] # Get default root state for this asset - root_states = asset.data.default_root_state[env_ids].clone() + root_states = asset.data.default_root_state.torch[env_ids].clone() # Apply position offset positions = root_states[:, 0:3] + env.scene.env_origins[env_ids] + rand_pose_samples[:, 0:3] if self.offset_asset_cfg: offset_asset: RigidObject | Articulation = env.scene[self.offset_asset_cfg.name] - offset_positions = offset_asset.data.default_root_state[env_ids].clone() + offset_positions = offset_asset.data.default_root_state.torch[env_ids].clone() positions += offset_positions[:, 0:3] if self.use_bottom_offset: @@ -1377,8 +1429,6 @@ def __call__( light_prim.GetAttribute("inputs:texture:file").Set(random_hdri) light_prim.GetAttribute("inputs:intensity").Set(float(intensity)) - from scipy.spatial.transform import Rotation as R - quat = R.random().as_quat() # [x, y, z, w] scipy convention xformable = UsdGeom.Xformable(light_prim) xformable.ClearXformOpOrder() @@ -1483,6 +1533,27 @@ def randomize_camera_focal_length( focal_attr.Set(focal_length) +def set_armature_from_sysid(env: ManagerBasedEnv, env_ids: torch.Tensor | None, asset_cfg: SceneEntityCfg) -> None: + """Set selected UR5e motor armatures [kg*m^2] from the robot's metadata. + + Args: + env: Environment containing the robot. + env_ids: Environments to update, or all environments when None. + asset_cfg: Robot and joint-name selection for the calibrated motor inertia. + """ + robot: Articulation = env.scene[asset_cfg.name] + metadata = utils.read_metadata_from_usd_directory(robot.cfg.spawn.usd_path) + nominal = metadata["sysid"]["armature"] + if len(nominal) != len(ARM_JOINT_NAMES): + raise ValueError("Expected one calibrated armature for each UR5e arm joint.") + joint_ids, joint_names = robot.find_joints(asset_cfg.joint_names) + values = torch.tensor( + [nominal[ARM_JOINT_NAMES.index(name)] for name in joint_names], device=robot.device, dtype=torch.float32 + ) + count = env.num_envs if env_ids is None else len(env_ids) + robot.write_joint_armature_to_sim_index(armature=values.repeat(count, 1), joint_ids=joint_ids, env_ids=env_ids) + + class randomize_arm_from_sysid(ManagerTermBase): """Randomize arm joint dynamics around sysid nominal values. @@ -1490,8 +1561,9 @@ class randomize_arm_from_sysid(ManagerTermBase): next to the robot USD. ``scale_range = (lo, hi)`` scales each nominal: ``nominal * uniform(lo, hi)`` per env per joint. - When used with ADR, ``scale_progress`` (0→1) linearly interpolates armature, - friction, and motor delay from 0 to the full sysid-randomized values. + When used with ADR, ``scale_progress`` (0 to 1) interpolates armature from + its startup value [kg*m^2] to the randomized calibration. Friction and motor + delay retain their zero-to-calibrated ramp. """ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): @@ -1509,8 +1581,9 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): self.dynamic_ratio = sysid["dynamic_ratio"] self.viscous_friction = sysid["viscous_friction"] - # ADR progress: 0 = armature/friction are 0, 1 = full sysid randomization + # ADR progress: 0 = startup armature and zero friction, 1 = full sysid randomization self.scale_progress: float = cfg.params.get("initial_scale_progress", 0.0) + self._initial_armature: torch.Tensor | None = None def __call__( self, @@ -1535,16 +1608,18 @@ def _scale(nominal): val = torch.as_tensor(nominal, device=device, dtype=torch.float32) return val * (lo + torch.rand(N, n_joints, device=device) * (hi - lo)) - # Armature and friction: scaled by ADR progress (0 → sysid) - arm_vals = _scale(self.armature) * p + # Armature and friction: interpolate from the startup model to sysid + if self._initial_armature is None: + self._initial_armature = self.robot.data.joint_armature.torch[:, self.joint_ids].clone() + arm_vals = self._initial_armature[env_ids] * (1.0 - p) + _scale(self.armature) * p sfric_vals = _scale(self.static_friction) * p dratio_vals = _scale(self.dynamic_ratio) * p dfric_vals = torch.minimum(dratio_vals * sfric_vals, sfric_vals) vfric_vals = _scale(self.viscous_friction) * p - self.robot.write_joint_armature_to_sim(arm_vals, joint_ids=self.joint_ids, env_ids=env_ids) - self.robot.write_joint_friction_coefficient_to_sim( - sfric_vals, + self.robot.write_joint_armature_to_sim_index(armature=arm_vals, joint_ids=self.joint_ids, env_ids=env_ids) + self.robot.write_joint_friction_coefficient_to_sim_index( + joint_friction_coeff=sfric_vals, joint_dynamic_friction_coeff=dfric_vals, joint_viscous_friction_coeff=vfric_vals, joint_ids=self.joint_ids, @@ -1633,10 +1708,12 @@ def _scale(nominal): gripper_actuator = self.robot.actuators[self.actuator_name] gripper_actuator.stiffness[env_ids] = stiff_vals gripper_actuator.damping[env_ids] = damp_vals - self.robot.write_joint_stiffness_to_sim(stiff_vals, joint_ids=self.gripper_joint_ids, env_ids=env_ids) - self.robot.write_joint_damping_to_sim(damp_vals, joint_ids=self.gripper_joint_ids, env_ids=env_ids) - self.robot.write_joint_armature_to_sim(arm_vals, joint_ids=self.gripper_joint_ids, env_ids=env_ids) - self.robot.write_joint_friction_coefficient_to_sim(fric_vals, joint_ids=self.gripper_joint_ids, env_ids=env_ids) + self.robot.write_joint_stiffness_to_sim_index(stiff_vals, joint_ids=self.gripper_joint_ids, env_ids=env_ids) + self.robot.write_joint_damping_to_sim_index(damp_vals, joint_ids=self.gripper_joint_ids, env_ids=env_ids) + self.robot.write_joint_armature_to_sim_index(arm_vals, joint_ids=self.gripper_joint_ids, env_ids=env_ids) + self.robot.write_joint_friction_coefficient_to_sim_index( + fric_vals, joint_ids=self.gripper_joint_ids, env_ids=env_ids + ) class randomize_rel_cartesian_osc_gains(ManagerTermBase): @@ -1739,7 +1816,7 @@ class adr_sysid_curriculum(ManagerTermBase): Monitors the mean success rate from ``MultiResetManager``'s ``SuccessMonitor`` and linearly ramps the ``scale_progress`` attribute of the target event terms - from 0 (no friction/armature) to 1 (full sysid randomization). + from 0 (startup dynamics) to 1 (full sysid randomization). Updates are gated by ``update_every_n_steps`` (env steps via ``common_step_counter``) to ensure the update rate is independent of the number of environments. @@ -1933,7 +2010,7 @@ class obs_noise_curriculum(ManagerTermBase): """Curriculum that gradually increases uniform noise on observation terms. Monitors success rate and linearly ramps the half-range on the specified - observation terms' ``AdditiveUniformNoiseCfg`` from ``initial_half_range`` + observation terms' ``UniformNoiseCfg`` from ``initial_half_range`` to ``target_half_range`` as progress goes from 0 to 1. At full progress the noise is U(-target_half_range, +target_half_range). """ @@ -1964,7 +2041,7 @@ def _resolve(self): cfg = name_to_cfg[name] if cfg.noise is None: raise ValueError( - f"Obs term '{name}' has no noise config. Set noise=AdditiveUniformNoiseCfg(n_min=0.0, n_max=0.0)." + f"Obs term '{name}' has no noise config. Set noise=UniformNoiseCfg(n_min=0.0, n_max=0.0)." ) self._obs_term_cfgs.append(cfg) @@ -2047,7 +2124,7 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): """Initialize the randomization term.""" super().__init__(cfg, env) - from isaacsim.core.utils.extensions import enable_extension + from isaaclab.sim.utils import enable_extension enable_extension("omni.replicator.core") import omni.replicator.core as rep @@ -2158,8 +2235,6 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): self._texture_verified = False # Cache shader prims for direct USD access (avoids Replicator pipeline race conditions) - from pxr import Sdf, UsdShade - self._shader_prims = [] for i, mat_prim in enumerate(self.material_prims): mat_path = str(mat_prim.GetPath()) if hasattr(mat_prim, "GetPath") else str(mat_prim) @@ -2180,6 +2255,11 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): "reflection_roughness_constant": Sdf.ValueTypeNames.Float, "metallic_constant": Sdf.ValueTypeNames.Float, "specular_level": Sdf.ValueTypeNames.Float, + # Isaac Sim 6 material templates do not author these until first use; USD 25.11 + # refuses Set() on an untyped attribute, so create them up front too. + "diffuse_texture": Sdf.ValueTypeNames.Asset, + "diffuse_tint": Sdf.ValueTypeNames.Color3f, + "diffuse_color_constant": Sdf.ValueTypeNames.Color3f, } for shader_prim in self._shader_prims: shader = UsdShade.Shader(shader_prim) @@ -2220,8 +2300,6 @@ def __call__( if not self._shader_prims: return - from pxr import Sdf - rng = self.texture_rng.generator num_prims = len(self._shader_prims) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/observations.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/observations.py index 43a64881..4f6a517b 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/observations.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/observations.py @@ -7,6 +7,7 @@ import torch.nn.functional as F import isaaclab.utils.math as math_utils +import warp as wp from isaaclab.assets import Articulation, RigidObject from isaaclab.envs import ManagerBasedEnv, ManagerBasedRLEnv from isaaclab.managers import ManagerTermBase, ObservationTermCfg, SceneEntityCfg @@ -30,10 +31,10 @@ def target_asset_pose_in_root_asset_frame( target_body_idx = 0 if isinstance(target_asset_cfg.body_ids, slice) else target_asset_cfg.body_ids root_body_idx = 0 if isinstance(root_asset_cfg.body_ids, slice) else root_asset_cfg.body_ids - target_pos = target_asset.data.body_link_pos_w[:, target_body_idx].view(-1, 3) - target_quat = target_asset.data.body_link_quat_w[:, target_body_idx].view(-1, 4) - root_pos = root_asset.data.body_link_pos_w[:, root_body_idx].view(-1, 3) - root_quat = root_asset.data.body_link_quat_w[:, root_body_idx].view(-1, 4) + target_pos = target_asset.data.body_link_pos_w.torch[:, target_body_idx].view(-1, 3) + target_quat = target_asset.data.body_link_quat_w.torch[:, target_body_idx].view(-1, 4) + root_pos = root_asset.data.body_link_pos_w.torch[:, root_body_idx].view(-1, 3) + root_quat = root_asset.data.body_link_quat_w.torch[:, root_body_idx].view(-1, 4) if root_asset_offset is not None: root_pos, root_quat = root_asset_offset.combine(root_pos, root_quat) @@ -102,10 +103,10 @@ def __call__( target_body_idx = 0 if isinstance(self.target_asset_cfg.body_ids, slice) else self.target_asset_cfg.body_ids root_body_idx = 0 if isinstance(self.root_asset_cfg.body_ids, slice) else self.root_asset_cfg.body_ids - target_pos = self.target_asset.data.body_link_pos_w[:, target_body_idx].view(-1, 3) - target_quat = self.target_asset.data.body_link_quat_w[:, target_body_idx].view(-1, 4) - root_pos = self.root_asset.data.body_link_pos_w[:, root_body_idx].view(-1, 3) - root_quat = self.root_asset.data.body_link_quat_w[:, root_body_idx].view(-1, 4) + target_pos = self.target_asset.data.body_link_pos_w.torch[:, target_body_idx].view(-1, 3) + target_quat = self.target_asset.data.body_link_quat_w.torch[:, target_body_idx].view(-1, 4) + root_pos = self.root_asset.data.body_link_pos_w.torch[:, root_body_idx].view(-1, 3) + root_quat = self.root_asset.data.body_link_quat_w.torch[:, root_body_idx].view(-1, 4) if self.root_asset_offset is not None: root_pos, root_quat = self.root_asset_offset.combine(root_pos, root_quat) @@ -133,26 +134,42 @@ def asset_link_velocity_in_root_asset_frame( target_body_idx = 0 if isinstance(target_asset_cfg.body_ids, slice) else target_asset_cfg.body_ids - asset_lin_vel_b, _ = math_utils.subtract_frame_transforms( - root_asset.data.root_pos_w, - root_asset.data.root_quat_w, - target_asset.data.body_lin_vel_w[:, target_body_idx].view(-1, 3), + root_quat_w = root_asset.data.root_quat_w.torch + + asset_lin_vel_b = math_utils.quat_apply_inverse( + root_quat_w, + target_asset.data.body_lin_vel_w.torch[:, target_body_idx].view(-1, 3), ) - asset_ang_vel_b, _ = math_utils.subtract_frame_transforms( - root_asset.data.root_pos_w, - root_asset.data.root_quat_w, - target_asset.data.body_ang_vel_w[:, target_body_idx].view(-1, 3), + asset_ang_vel_b = math_utils.quat_apply_inverse( + root_quat_w, + target_asset.data.body_ang_vel_w.torch[:, target_body_idx].view(-1, 3), ) return torch.cat([asset_lin_vel_b, asset_ang_vel_b], dim=1) +def _as_torch(value) -> torch.Tensor: + """Return ``value`` as a torch tensor. + + Isaac Lab 3.0 hands back warp-backed data from both asset ``.data`` properties + (as :class:`ProxyArray`) and physics view accessors (as raw ``wp.array``). + Neither supports torch instance methods such as ``view()`` -- ``wp.array.view()`` + is a dtype reinterpret with a different signature -- so convert explicitly. + """ + if hasattr(value, "torch"): # ProxyArray + return value.torch + if isinstance(value, wp.array): + return wp.to_torch(value) + return value + + def get_material_properties( env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg, ): + """Per-shape (static friction, dynamic friction, restitution), flattened.""" asset: RigidObject | Articulation = env.scene[asset_cfg.name] - return asset.root_physx_view.get_material_properties().view(env.num_envs, -1) + return _as_torch(asset.root_view.get_material_properties()).view(env.num_envs, -1) def get_mass( @@ -160,7 +177,7 @@ def get_mass( asset_cfg: SceneEntityCfg, ): asset: RigidObject | Articulation = env.scene[asset_cfg.name] - return asset.root_physx_view.get_masses().view(env.num_envs, -1) + return _as_torch(asset.root_view.get_masses()).view(env.num_envs, -1) def get_joint_friction( @@ -168,7 +185,7 @@ def get_joint_friction( asset_cfg: SceneEntityCfg, ): asset: RigidObject | Articulation = env.scene[asset_cfg.name] - return asset.data.joint_friction_coeff.view(env.num_envs, -1) + return asset.data.joint_friction_coeff.torch.view(env.num_envs, -1) def get_joint_armature( @@ -176,7 +193,7 @@ def get_joint_armature( asset_cfg: SceneEntityCfg, ): asset: RigidObject | Articulation = env.scene[asset_cfg.name] - return asset.data.joint_armature.view(env.num_envs, -1) + return asset.data.joint_armature.torch.view(env.num_envs, -1) def get_joint_stiffness( @@ -184,7 +201,7 @@ def get_joint_stiffness( asset_cfg: SceneEntityCfg, ): asset: RigidObject | Articulation = env.scene[asset_cfg.name] - return asset.data.joint_stiffness.view(env.num_envs, -1) + return asset.data.joint_stiffness.torch.view(env.num_envs, -1) def get_joint_damping( @@ -192,7 +209,7 @@ def get_joint_damping( asset_cfg: SceneEntityCfg, ): asset: RigidObject | Articulation = env.scene[asset_cfg.name] - return asset.data.joint_damping.view(env.num_envs, -1) + return asset.data.joint_damping.torch.view(env.num_envs, -1) def time_left(env) -> torch.Tensor: @@ -275,27 +292,26 @@ def process_image( def binary_force_contact( env: ManagerBasedEnv, - asset_cfg: SceneEntityCfg, + sensor_cfg: SceneEntityCfg = SceneEntityCfg("joint_wrench"), body_name: str = "wrist_3_link", force_threshold: float = 25.0, ) -> torch.Tensor: - """Binary contact detection from force norm at a body. + """Binary contact detection from the incoming joint force norm at a body. - Reads body_incoming_joint_wrench_b, computes ||F|| from the force - components (first 3), and returns 1.0 if above threshold, else 0.0. + Reads the force reported by a :class:`~isaaclab.sensors.JointWrenchSensor` on the + robot, computes ||F|| and returns 1.0 if above threshold, else 0.0. Args: env: The environment. - asset_cfg: Scene entity config for the robot articulation. - body_name: Name of the body to read wrench from. + sensor_cfg: Scene entity config for the joint-wrench sensor on the robot. + body_name: Name of the body to read the wrench from. force_threshold: Force norm threshold (N) for contact detection. Returns: Tensor of shape (num_envs, 1): 1.0 if contact, 0.0 otherwise. """ - robot: Articulation = env.scene[asset_cfg.name] - body_idx = robot.body_names.index(body_name) - wrench_b = robot.data.body_incoming_joint_wrench_b[:, body_idx, :] # (N, 6) - force_norm = torch.norm(wrench_b[:, :3], dim=-1) # (N,) + sensor = env.scene.sensors[sensor_cfg.name] + body_idx = sensor.find_bodies(body_name)[0][0] + force_norm = torch.norm(sensor.data.force.torch[:, body_idx], dim=-1) # (N,) contact = (force_norm > force_threshold).float() return contact.unsqueeze(-1) # (N, 1) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/recorders/recorders.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/recorders/recorders.py index 0da795bb..41c27660 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/recorders/recorders.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/recorders/recorders.py @@ -45,9 +45,9 @@ def record_pre_reset(self, env_ids): obj = self._env.scene[self.object_name] # Get object pose (root pose contains position and orientation) - obj_root_state = obj.data.root_state_w[env_ids] # Shape: (num_envs, 13) - pos(3) + quat(4) + vel(6) + obj_root_state = obj.data.root_state_w.torch[env_ids] # (num_envs, 13) - pos(3) + quat(4) + vel(6) obj_pos = obj_root_state[:, :3] # Position - obj_quat = obj_root_state[:, 3:7] # Quaternion (w, x, y, z) + obj_quat = obj_root_state[:, 3:7] # Get gripper body pose from the robot articulation # Find the gripper body index @@ -58,14 +58,14 @@ def record_pre_reset(self, env_ids): break # Get specific body pose - gripper_pos = robot.data.body_state_w[env_ids, gripper_body_idx, :3] - gripper_quat = robot.data.body_state_w[env_ids, gripper_body_idx, 3:7] + gripper_pos = robot.data.body_state_w.torch[env_ids, gripper_body_idx, :3] + gripper_quat = robot.data.body_state_w.torch[env_ids, gripper_body_idx, 3:7] # Calculate relative transform: T_gripper_in_object = T_object^{-1} * T_gripper relative_pos, relative_quat = math_utils.subtract_frame_transforms(obj_pos, obj_quat, gripper_pos, gripper_quat) # Get gripper joint states as dict mapping joint names to positions - gripper_joint_pos = robot.data.joint_pos[env_ids].clone() + gripper_joint_pos = robot.data.joint_pos.torch[env_ids].clone() gripper_joint_dict = {joint_name: gripper_joint_pos[:, i] for i, joint_name in enumerate(robot.joint_names)} # Prepare data to record diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/rewards.py index 18cfbc2d..2814541b 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/rewards.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/rewards.py @@ -59,12 +59,12 @@ def __call__( std: float = 0.1, ) -> torch.Tensor: root_asset_alignment_pos_w, root_asset_alignment_quat_w = self.root_asset_offset.combine( - self.root_asset.data.body_link_pos_w[:, root_asset_cfg.body_ids].view(-1, 3), - self.root_asset.data.body_link_quat_w[:, root_asset_cfg.body_ids].view(-1, 4), + self.root_asset.data.body_link_pos_w.torch[:, root_asset_cfg.body_ids].view(-1, 3), + self.root_asset.data.body_link_quat_w.torch[:, root_asset_cfg.body_ids].view(-1, 4), ) if self.target_asset_offset is None: - target_asset_alignment_pos_w = self.target_asset.data.root_pos_w.view(-1, 3) - target_asset_alignment_quat_w = self.target_asset.data.root_quat_w.view(-1, 4) + target_asset_alignment_pos_w = self.target_asset.data.root_pos_w.torch.view(-1, 3) + target_asset_alignment_quat_w = self.target_asset.data.root_quat_w.torch.view(-1, 4) else: target_asset_alignment_pos_w, target_asset_alignment_quat_w = self.target_asset_offset.apply( self.target_asset @@ -112,7 +112,9 @@ def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): def reset(self, env_ids: torch.Tensor | None = None) -> None: super().reset(env_ids) - self.continuous_success_counter[:] = 0 + if env_ids is None: + env_ids = slice(None) + self.continuous_success_counter[env_ids] = 0 def __call__( self, diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/rigid_object_hasher.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/rigid_object_hasher.py index 4f6b16bf..d27ebb9b 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/rigid_object_hasher.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/rigid_object_hasher.py @@ -5,12 +5,12 @@ import hashlib import numpy as np +import re import torch -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils +import isaaclab.sim.utils.stage as stage_utils import warp as wp -from isaaclab.sim import get_all_matching_child_prims +from isaaclab.sim import find_matching_prim_paths, get_all_matching_child_prims from pxr import Gf, Usd, UsdGeom, UsdPhysics HASH_STORE = {"warp_mesh_store": {}, "__stage_id__": None} @@ -45,8 +45,9 @@ def __init__(self, num_envs, prim_path_pattern, device="cpu"): "root_prim_scales": [], } stor = HASH_STORE[prim_path_pattern] + stage = stage_utils.get_current_stage() xform_cache = UsdGeom.XformCache() - prim_paths = [prim_path_pattern.replace(".*", f"{i}", 1) for i in range(num_envs)] + prim_paths = self.resolve_prim_paths(num_envs, prim_path_pattern, stage=stage) num_roots = len(prim_paths) collider_prim_env_ids = [] @@ -69,7 +70,7 @@ def __init__(self, num_envs, prim_path_pattern, device="cpu"): collider_prim_env_ids.extend([i] * len(coll_prims)) # 2: Get relative transforms of all collider prims - root_xf = xform_cache.GetLocalToWorldTransform(prim_utils.get_prim_at_path(prim_paths[i])) + root_xf = xform_cache.GetLocalToWorldTransform(stage.GetPrimAtPath(prim_paths[i])) root_tf = Gf.Transform(root_xf) rel_tfs = [] root_prim_scales.append(torch.tensor(root_tf.GetScale())) @@ -78,7 +79,7 @@ def __init__(self, num_envs, prim_path_pattern, device="cpu"): rel_mat_tf = Gf.Transform(child_xf * root_xf.GetInverse()) rel_quat = rel_mat_tf.GetRotation().GetQuat() rel_t = torch.tensor(rel_mat_tf.GetTranslation()) - rel_q = torch.tensor([rel_quat.GetReal(), *rel_quat.GetImaginary()]) + rel_q = torch.tensor([*rel_quat.GetImaginary(), rel_quat.GetReal()]) rel_s = torch.tensor(rel_mat_tf.GetScale()) rel_tfs.append(torch.cat([rel_t, rel_q, rel_s])) rel_tfs = torch.cat(rel_tfs) @@ -86,7 +87,7 @@ def __init__(self, num_envs, prim_path_pattern, device="cpu"): # 3: Store the collider prims hash root_hash = hashlib.sha256() - for prim, prim_rel_tf in zip(coll_prims, rel_tfs.numpy()): + for prim, prim_rel_tf in zip(coll_prims, rel_tfs.view(-1, 10).numpy()): h = hashlib.sha256() h.update( np.round(prim_rel_tf * 50).astype(np.int64) @@ -130,6 +131,16 @@ def __init__(self, num_envs, prim_path_pattern, device="cpu"): stor["root_prim_hashes"] = torch.tensor(root_prim_hashes, dtype=torch.int64, device="cpu") stor["root_prim_scales"] = torch.stack(root_prim_scales).to("cpu") + @staticmethod + def resolve_prim_paths(num_envs: int, prim_path_pattern: str, stage: Usd.Stage | None = None) -> list[str]: + """Resolve one asset root per environment in natural environment-index order.""" + stage = stage_utils.get_current_stage() if stage is None else stage + paths = find_matching_prim_paths(prim_path_pattern, stage=stage) + paths.sort(key=lambda path: [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", path)]) + if len(paths) != num_envs: + raise ValueError(f"Expected {num_envs} asset roots for {prim_path_pattern}, found {len(paths)}.") + return paths + @property def num_root(self) -> int: return self.get_val("num_roots") diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/terminations.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/terminations.py index 7daf6855..a1de88a3 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/terminations.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/terminations.py @@ -5,13 +5,12 @@ """MDP functions for manipulation tasks.""" -import numpy as np import torch -import isaacsim.core.utils.bounds as bounds_utils -from isaaclab.assets import Articulation, RigidObject, RigidObjectCollection +from isaaclab.assets import BaseArticulation, BaseRigidObject, BaseRigidObjectCollection from isaaclab.envs import ManagerBasedEnv, ManagerBasedRLEnv from isaaclab.managers import ManagerTermBase, SceneEntityCfg, TerminationTermCfg +from isaaclab.sim.utils import enable_extension from isaaclab.utils import math as math_utils from uwlab_tasks.manager_based.manipulation.omnireset.mdp import utils @@ -135,11 +134,11 @@ def reset(self, env_ids: torch.Tensor | None = None) -> None: object_asset = self._env.scene[self.object_cfg.name] if not hasattr(object_asset, "initial_pos"): - object_asset.initial_pos = object_asset.data.root_pos_w.clone() - object_asset.initial_quat = object_asset.data.root_quat_w.clone() + object_asset.initial_pos = object_asset.data.root_pos_w.torch.clone() + object_asset.initial_quat = object_asset.data.root_quat_w.torch.clone() else: - object_asset.initial_pos[env_ids] = object_asset.data.root_pos_w[env_ids].clone() - object_asset.initial_quat[env_ids] = object_asset.data.root_quat_w[env_ids].clone() + object_asset.initial_pos[env_ids] = object_asset.data.root_pos_w.torch[env_ids].clone() + object_asset.initial_quat[env_ids] = object_asset.data.root_quat_w.torch[env_ids].clone() if env_ids is None: self.stability_counter.zero_() @@ -164,21 +163,21 @@ def __call__( time_out = env.episode_length_buf >= env.max_episode_length # Check for abnormal gripper state (excessive joint velocities) - abnormal_gripper_state = (gripper_asset.data.joint_vel.abs() > (gripper_asset.data.joint_vel_limits * 2)).any( - dim=1 - ) + abnormal_gripper_state = ( + gripper_asset.data.joint_vel.torch.abs() > (gripper_asset.data.joint_vel_limits.torch * 2) + ).any(dim=1) # Check if asset velocities are small current_step_stable = torch.ones(env.num_envs, device=env.device, dtype=torch.bool) # Check gripper (articulation) velocities - current_step_stable &= gripper_asset.data.joint_vel.abs().sum(dim=1) < 5.0 + current_step_stable &= gripper_asset.data.joint_vel.torch.abs().sum(dim=1) < 5.0 # Check object (rigid object) velocities - if isinstance(object_asset, RigidObject): - current_step_stable &= object_asset.data.body_lin_vel_w.abs().sum(dim=2).sum(dim=1) < 0.05 - current_step_stable &= object_asset.data.body_ang_vel_w.abs().sum(dim=2).sum(dim=1) < 1.0 - elif isinstance(object_asset, RigidObjectCollection): - current_step_stable &= object_asset.data.object_lin_vel_w.abs().sum(dim=2).sum(dim=1) < 0.05 - current_step_stable &= object_asset.data.object_ang_vel_w.abs().sum(dim=2).sum(dim=1) < 1.0 + if isinstance(object_asset, BaseRigidObject): + current_step_stable &= object_asset.data.body_lin_vel_w.torch.abs().sum(dim=2).sum(dim=1) < 0.05 + current_step_stable &= object_asset.data.body_ang_vel_w.torch.abs().sum(dim=2).sum(dim=1) < 1.0 + elif isinstance(object_asset, BaseRigidObjectCollection): + current_step_stable &= object_asset.data.object_lin_vel_w.torch.abs().sum(dim=2).sum(dim=1) < 0.05 + current_step_stable &= object_asset.data.object_ang_vel_w.torch.abs().sum(dim=2).sum(dim=1) < 1.0 self.stability_counter = torch.where( current_step_stable, @@ -189,17 +188,17 @@ def __call__( stability_reached = self.stability_counter >= self.consecutive_stability_steps # Skip if position or quaternion is NaN - pos_is_nan = torch.isnan(object_asset.data.root_pos_w).any(dim=1) - quat_is_nan = torch.isnan(object_asset.data.root_quat_w).any(dim=1) + pos_is_nan = torch.isnan(object_asset.data.root_pos_w.torch).any(dim=1) + quat_is_nan = torch.isnan(object_asset.data.root_quat_w.torch).any(dim=1) skip_check = pos_is_nan | quat_is_nan # Object has excessive pose deviation if position exceeds thresholds - pos_deviation = (object_asset.data.root_pos_w - object_asset.initial_pos).norm(dim=1) + pos_deviation = (object_asset.data.root_pos_w.torch - object_asset.initial_pos).norm(dim=1) valid_pos_deviation = torch.where(~skip_check, pos_deviation, torch.zeros_like(pos_deviation)) excessive_pose_deviation = valid_pos_deviation > self.max_pos_deviation # Object is above ground if position is greater than z threshold - pos_above_ground = object_asset.data.root_pos_w[:, 2] >= self.pos_z_threshold + pos_above_ground = object_asset.data.root_pos_w.torch[:, 2] >= self.pos_z_threshold # Check for collisions between gripper and object all_env_ids = torch.arange(env.num_envs, device=env.device) @@ -285,9 +284,9 @@ def reset(self, env_ids: torch.Tensor | None = None) -> None: for asset in self.assets_to_check: if asset is self.robot_asset: - asset_pos = asset.data.body_link_pos_w[:, self.ee_body_idx].clone() + asset_pos = asset.data.body_link_pos_w.torch[:, self.ee_body_idx].clone() else: - asset_pos = asset.data.root_pos_w.clone() + asset_pos = asset.data.root_pos_w.torch.clone() if not hasattr(asset, "initial_pos") or env_ids is None: asset.initial_pos = asset_pos else: @@ -335,11 +334,11 @@ def __call__( # Check for abnormal gripper state (excessive joint velocities) abnormal_gripper_state = ( - self.robot_asset.data.joint_vel.abs() > (self.robot_asset.data.joint_vel_limits * 2) + self.robot_asset.data.joint_vel.torch.abs() > (self.robot_asset.data.joint_vel_limits.torch * 2) ).any(dim=1) # Check if gripper orientation is pointing downward within 60 degrees of vertical - ee_quat = self.robot_asset.data.body_link_quat_w[:, self.ee_body_idx] + ee_quat = self.robot_asset.data.body_link_quat_w.torch[:, self.ee_body_idx] gripper_approach_local = torch.tensor( self.gripper_approach_direction, device=env.device, dtype=torch.float32 ).expand(env.num_envs, -1) @@ -351,14 +350,14 @@ def __call__( # Check if asset velocities are small current_step_stable = torch.ones(env.num_envs, device=env.device, dtype=torch.bool) for asset in self.assets_to_check: - if isinstance(asset, Articulation): - current_step_stable &= asset.data.joint_vel.abs().sum(dim=1) < 5.0 - elif isinstance(asset, RigidObject): - current_step_stable &= asset.data.body_lin_vel_w.abs().sum(dim=2).sum(dim=1) < 0.1 - current_step_stable &= asset.data.body_ang_vel_w.abs().sum(dim=2).sum(dim=1) < 1.0 - elif isinstance(asset, RigidObjectCollection): - current_step_stable &= asset.data.object_lin_vel_w.abs().sum(dim=2).sum(dim=1) < 0.1 - current_step_stable &= asset.data.object_ang_vel_w.abs().sum(dim=2).sum(dim=1) < 1.0 + if isinstance(asset, BaseArticulation): + current_step_stable &= asset.data.joint_vel.torch.abs().sum(dim=1) < 5.0 + elif isinstance(asset, BaseRigidObject): + current_step_stable &= asset.data.body_lin_vel_w.torch.abs().sum(dim=2).sum(dim=1) < 0.1 + current_step_stable &= asset.data.body_ang_vel_w.torch.abs().sum(dim=2).sum(dim=1) < 1.0 + elif isinstance(asset, BaseRigidObjectCollection): + current_step_stable &= asset.data.object_lin_vel_w.torch.abs().sum(dim=2).sum(dim=1) < 0.1 + current_step_stable &= asset.data.object_ang_vel_w.torch.abs().sum(dim=2).sum(dim=1) < 1.0 self.stability_counter = torch.where( current_step_stable, @@ -373,13 +372,13 @@ def __call__( pos_below_threshold = torch.zeros(env.num_envs, device=env.device, dtype=torch.bool) for asset in self.assets_to_check: if asset is self.robot_asset: - asset_pos = asset.data.body_link_pos_w[:, self.ee_body_idx].clone() + asset_pos = asset.data.body_link_pos_w.torch[:, self.ee_body_idx].clone() else: - asset_pos = asset.data.root_pos_w.clone() + asset_pos = asset.data.root_pos_w.torch.clone() # Skip if position or quaternion is NaN - pos_is_nan = torch.isnan(asset.data.root_pos_w).any(dim=1) - quat_is_nan = torch.isnan(asset.data.root_quat_w).any(dim=1) + pos_is_nan = torch.isnan(asset.data.root_pos_w.torch).any(dim=1) + quat_is_nan = torch.isnan(asset.data.root_quat_w.torch).any(dim=1) skip_check = pos_is_nan | quat_is_nan # Asset has excessive pose deviation if position exceeds thresholds @@ -437,6 +436,10 @@ def __init__(self, cfg: TerminationTermCfg, env: ManagerBasedEnv): self.insertive_object = env.scene[self.insertive_object_cfg.name] self.enable_visualization = cfg.params.get("enable_visualization", False) + enable_extension("isaacsim.core.experimental.utils") + import isaacsim.core.experimental.utils.bounds as bounds_utils + + self._bounds_utils = bounds_utils # Initialize OBB computation cache and compute OBBs once self._bbox_cache = bounds_utils.create_bbox_cache() @@ -448,6 +451,7 @@ def __init__(self, cfg: TerminationTermCfg, env: ManagerBasedEnv): # Store debug draw interface if visualization is enabled if self.enable_visualization: + enable_extension("isaacsim.util.debug_draw") import isaacsim.util.debug_draw._debug_draw as omni_debug_draw self._omni_debug_draw = omni_debug_draw @@ -457,16 +461,18 @@ def __init__(self, cfg: TerminationTermCfg, env: ManagerBasedEnv): def _compute_object_obbs(self): """Compute OBB for insertive object and convert to body frame.""" # Get prim path (use env 0 as template) - insertive_prim_path = self.insertive_object.cfg.prim_path.replace(".*", "0", 1) + insertive_prim_path = utils.RigidObjectHasher.resolve_prim_paths( + self._env.num_envs, self.insertive_object.cfg.prim_path + )[0] # Compute OBB in world frame using Isaac Sim's built-in functions - insertive_centroid_world, insertive_axes_world, insertive_half_extents = bounds_utils.compute_obb( - self._bbox_cache, insertive_prim_path + insertive_centroid_world, insertive_axes_world, insertive_half_extents = self._bounds_utils.compute_obb( + insertive_prim_path, bbox_cache=self._bbox_cache ) # Get current world pose of object (env 0) to convert OBB to body frame - insertive_pos_world = self.insertive_object.data.root_pos_w[0] # (3,) - insertive_quat_world = self.insertive_object.data.root_quat_w[0] # (4,) + insertive_pos_world = self.insertive_object.data.root_pos_w.torch[0] # (3,) + insertive_quat_world = self.insertive_object.data.root_quat_w.torch[0] # (4,) device = self._env.device @@ -495,8 +501,8 @@ def reset(self, env_ids: torch.Tensor | None = None) -> None: """Store initial pose of insertive object when environments are reset.""" super().reset(env_ids) - insertive_pos = self.insertive_object.data.root_pos_w.clone() - insertive_quat = self.insertive_object.data.root_quat_w.clone() + insertive_pos = self.insertive_object.data.root_pos_w.torch.clone() + insertive_quat = self.insertive_object.data.root_quat_w.torch.clone() if self._insertive_initial_pos is None or self._insertive_initial_quat is None or env_ids is None: # First time initialization or reset all environments @@ -507,38 +513,37 @@ def reset(self, env_ids: torch.Tensor | None = None) -> None: self._insertive_initial_pos[env_ids] = insertive_pos[env_ids] self._insertive_initial_quat[env_ids] = insertive_quat[env_ids] - def _compute_obb_corners_batch(self, centroids, axes, half_extents): - """ - Compute the 8 corners of Oriented Bounding Boxes for all environments using Isaac Sim's built-in function. + @torch.no_grad() + def _compute_obb_corners_batch( + self, centroids: torch.Tensor, axes: torch.Tensor, half_extents: torch.Tensor + ) -> torch.Tensor: + """Compute batched OBB corners on-device in Isaac Sim's corner order. Args: - centroids: Centers of OBBs (num_envs, 3) - axes: Orientation axes of OBBs (num_envs, 3, 3) - rows are the axes - half_extents: Half extents of OBB along its axes (3,) + centroids: Centers [m], shape (num_envs, 3). + axes: Orientation axes, shape (num_envs, 3, 3), with one axis per row. + half_extents: Half lengths [m], shape (3,). Returns: - corners: 8 corners of the OBBs (num_envs, 8, 3) + Detached float32 corner positions [m], shape (num_envs, 8, 3). """ num_envs = centroids.shape[0] device = centroids.device - # Convert torch tensors to numpy for Isaac Sim functions - centroids_np = centroids.detach().cpu().numpy() - axes_np = axes.detach().cpu().numpy() - half_extents_np = half_extents.detach().cpu().numpy() + # Keep corner offsets on the input device. + signs = torch.tensor( + [[-1, -1, -1], [-1, -1, 1], [-1, 1, -1], [-1, 1, 1], [1, -1, -1], [1, -1, 1], [1, 1, -1], [1, 1, 1]], + device=device, + dtype=centroids.dtype, + ) - # Compute corners for each environment using Isaac Sim's function - all_corners = [] - for env_idx in range(num_envs): - # Use Isaac Sim's get_obb_corners function - corners_np = bounds_utils.get_obb_corners( - centroids_np[env_idx], axes_np[env_idx], half_extents_np - ) # (8, 3) - all_corners.append(corners_np) + # Compute offsets for all environments at once. + offsets = (signs * half_extents).expand(num_envs, -1, -1) + # Match Isaac Sim's ordering and row-vector axes. + corners_tensor = centroids.unsqueeze(1) + torch.bmm(offsets, axes) # (num_envs, 8, 3) - # Convert back to torch tensor - corners_tensor = torch.tensor(np.stack(all_corners), device=device, dtype=torch.float32) - return corners_tensor # (num_envs, 8, 3) + # Preserve the existing float32 output contract without a host transfer. + return corners_tensor.to(dtype=torch.float32) # (num_envs, 8, 3) def _visualize_bounding_boxes(self, env: ManagerBasedEnv): """Visualize oriented bounding boxes for initial and current insertive object positions using wireframe edges.""" @@ -547,8 +552,8 @@ def _visualize_bounding_boxes(self, env: ManagerBasedEnv): draw_interface.clear_lines() # Get current world poses of insertive object for all environments - insertive_pos = self.insertive_object.data.root_pos_w # (num_envs, 3) - insertive_quat = self.insertive_object.data.root_quat_w # (num_envs, 4) + insertive_pos = self.insertive_object.data.root_pos_w.torch # (num_envs, 3) + insertive_quat = self.insertive_object.data.root_quat_w.torch # (num_envs, 4) # Transform current insertive object OBB centroid from body frame to world coordinates for all environments insertive_obb_centroid_body = self._insertive_obb_centroid @@ -566,7 +571,7 @@ def _visualize_bounding_boxes(self, env: ManagerBasedEnv): 1, 2 ) # (num_envs, 3, 3) - # Compute OBB corners for current position visualization using Isaac Sim's built-in function + # Compute OBB corners for current position visualization in one batch. insertive_current_corners = self._compute_obb_corners_batch( insertive_current_world_centroids, insertive_current_world_axes, self._insertive_obb_half_extents ) # (num_envs, 8, 3) @@ -585,28 +590,27 @@ def _visualize_bounding_boxes(self, env: ManagerBasedEnv): 1, 2 ) # (num_envs, 3, 3) - # Compute OBB corners for initial position visualization using Isaac Sim's built-in function + # Compute OBB corners for initial position visualization in one batch. insertive_initial_corners = self._compute_obb_corners_batch( insertive_initial_world_centroids, insertive_initial_world_axes, self._insertive_obb_half_extents ) # (num_envs, 8, 3) - # Draw wireframe boxes for each environment - for env_idx in range(env.num_envs): - # Draw current insertive object bounding box edges (blue) - self._draw_obb_wireframe( - insertive_current_corners[env_idx], # (8, 3) - color=(0.0, 0.5, 1.0, 1.0), # Bright blue - line_width=4.0, - draw_interface=draw_interface, - ) + # Draw wireframe boxes for the environment batch. + # Draw current insertive object bounding box edges (blue) + self._draw_obb_wireframe( + insertive_current_corners, # (num_envs, 8, 3) + color=(0.0, 0.5, 1.0, 1.0), # Bright blue + line_width=4.0, + draw_interface=draw_interface, + ) - # Draw initial insertive object bounding box edges (red) - self._draw_obb_wireframe( - insertive_initial_corners[env_idx], # (8, 3) - color=(1.0, 0.2, 0.0, 1.0), # Bright red - line_width=4.0, - draw_interface=draw_interface, - ) + # Draw initial insertive object bounding box edges (red) + self._draw_obb_wireframe( + insertive_initial_corners, # (num_envs, 8, 3) + color=(1.0, 0.2, 0.0, 1.0), # Bright red + line_width=4.0, + draw_interface=draw_interface, + ) def _draw_obb_wireframe( self, corners: torch.Tensor, color: tuple = (1.0, 1.0, 1.0, 1.0), line_width: float = 2.0, draw_interface=None @@ -615,7 +619,7 @@ def _draw_obb_wireframe( Draw wireframe edges of an oriented bounding box. Args: - corners: 8 corners of the OBB (8, 3) + corners: Corner positions [m], shape (num_envs, 8, 3) or (8, 3). color: RGBA color tuple for the lines line_width: Width of the lines draw_interface: Debug draw interface (optional, will acquire if not provided) @@ -658,20 +662,18 @@ def _draw_obb_wireframe( (2, 5), ] - # Create line segments for all edges - line_starts = [] - line_ends = [] - - for start_idx, end_idx in edge_indices: - line_starts.append(corners[start_idx].cpu().numpy().tolist()) - line_ends.append(corners[end_idx].cpu().numpy().tolist()) + # Create all line segments on-device, then transfer once for the debug-draw API. + edges = torch.tensor(edge_indices, device=corners.device) + lines = corners.reshape(-1, 8, 3)[:, edges].reshape(-1, 2, 3).detach().cpu() + line_starts = lines[:, 0].tolist() + line_ends = lines[:, 1].tolist() # Use provided interface or acquire new one if draw_interface is None: draw_interface = self._omni_debug_draw.acquire_debug_draw_interface() - colors = [list(color)] * len(edge_indices) - line_thicknesses = [line_width] * len(edge_indices) + colors = [list(color)] * len(line_starts) + line_thicknesses = [line_width] * len(line_starts) # Draw all edges at once draw_interface.draw_lines(line_starts, line_ends, colors, line_thicknesses) @@ -685,8 +687,8 @@ def __call__( """Check if OBB overlap condition is violated between initial and current insertive object positions.""" # Get current world poses of insertive object for all environments - insertive_pos = self.insertive_object.data.root_pos_w # (num_envs, 3) - insertive_quat = self.insertive_object.data.root_quat_w # (num_envs, 4) + insertive_pos = self.insertive_object.data.root_pos_w.torch # (num_envs, 3) + insertive_quat = self.insertive_object.data.root_quat_w.torch # (num_envs, 4) # Transform current insertive object centroid from body frame to world coordinates for all environments insertive_obb_centroid_body = self._insertive_obb_centroid diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/utils.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/utils.py index 86fe3f40..811570b9 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/utils.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/omnireset/mdp/utils.py @@ -12,25 +12,25 @@ import random import shutil import tempfile +import time import torch import trimesh import yaml +from concurrent.futures import ThreadPoolExecutor, as_completed from contextlib import contextmanager, redirect_stderr, redirect_stdout from functools import lru_cache from pathlib import PurePosixPath from urllib.parse import urlparse import isaaclab.utils.math as math_utils -import isaacsim.core.utils.torch as torch_utils import omni import warp as wp from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, NVIDIA_NUCLEUS_DIR, retrieve_file_path +from isaaclab.utils.seed import configure_seed from isaaclab.utils.warp import convert_to_warp_mesh from pxr import UsdGeom -from pytorch3d.ops import sample_farthest_points, sample_points_from_meshes -from pytorch3d.structures import Meshes -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR, _extract_relative_path, resolve_cloud_path from .rigid_object_hasher import RigidObjectHasher @@ -84,6 +84,23 @@ def sample_object_point_cloud( Returns: torch.Tensor | None: _description_ """ + # Imported lazily: pytorch3d is an optional, hard-to-build dependency (it ships as a + # wheel compiled against an exact python/torch/CUDA combination, and there is no sdist + # fallback). The only caller is CollisionAnalyzer, i.e. the dataset-generation tasks + # (reset states, grasp sampling, partial assemblies); the RL envs never reach it. A + # module-level import would make every task in the package unimportable wherever no + # matching wheel exists. See the wheel table in uwlab_tasks/setup.py. + try: + from pytorch3d.ops import sample_farthest_points, sample_points_from_meshes + from pytorch3d.structures import Meshes + except ImportError as exc: + raise ImportError( + "sample_object_point_cloud() requires pytorch3d, which is not installed. It is " + "optional and only needed by the collision analyzer used when generating datasets; " + "see the wheel table in uwlab_tasks/setup.py for the supported python/torch/CUDA " + "combinations." + ) from exc + hasher = ( rigid_object_hasher if rigid_object_hasher is not None @@ -295,7 +312,7 @@ def temporary_seed(seed: int, restore_numpy: bool = True, restore_python: bool = try: sink = io.StringIO() with redirect_stdout(sink), redirect_stderr(sink): - torch_utils.set_seed(seed) + configure_seed(seed) yield finally: # restore everything @@ -339,8 +356,6 @@ def safe_retrieve_file_path(url: str, download_dir: str | None = None) -> str: handles download + persistent caching. Nucleus (``omniverse://``) paths still fall back to Isaac Lab's :func:`retrieve_file_path`. """ - from uwlab_assets import resolve_cloud_path - if url.startswith(("http://", "https://")) or os.path.isfile(url): return resolve_cloud_path(url) @@ -374,6 +389,14 @@ def read_metadata_from_usd_directory(usd_path: str) -> dict: with open(local_path) as f: metadata_file = yaml.safe_load(f) + # Isaac Lab 3.0 reads quaternions as (x, y, z, w); metadata authored under 2.x is (w, x, y, z) + # and would be applied verbatim, i.e. silently rotated. Require the converted, stamped files + # published on the ``isaaclab3`` branch of the cloud asset repository. + if metadata_file.pop("quat_convention", None) != "xyzw": + raise ValueError( + f"{metadata_path} is not an Isaac Lab 3.0 metadata file (missing 'quat_convention: xyzw');" + " point UWLAB_CLOUD_ASSETS_REVISION at a commit on the isaaclab3 cloud asset branch" + ) return metadata_file @@ -512,11 +535,6 @@ def _download_cloud_assets(cloud_urls: list[str], cache_subdir: str = "", num_wo Downloads are parallelized with *num_workers* threads and a live progress line with elapsed time is printed. """ - import time - from concurrent.futures import ThreadPoolExecutor, as_completed - - from uwlab_assets import resolve_cloud_path - n = len(cloud_urls) to_download = [u for u in cloud_urls if not os.path.isfile(_cached_local_path(u))] needs_download = len(to_download) @@ -553,8 +571,6 @@ def _download_cloud_assets(cloud_urls: list[str], cache_subdir: str = "", num_wo def _cached_local_path(url: str) -> str: """Return the expected local cache path for a cloud URL without downloading.""" - from uwlab_assets import _extract_relative_path - rel = _extract_relative_path(url) return os.path.join(os.path.expanduser("~"), ".cache", "uwlab", "assets", rel) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/track_goal_ur5_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/track_goal_ur5_env_cfg.py index d02f61f6..12b9e418 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/track_goal_ur5_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/track_goal_ur5_env_cfg.py @@ -5,6 +5,7 @@ from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.utils import configclass +from isaaclab_physx.physics import PhysxCfg import uwlab_assets.robots.ur5 as ur5 @@ -34,7 +35,9 @@ def __post_init__(self): self.viewer.eye = (3.0, 3.0, 1.0) # Contact and solver settings - self.sim.physx.solver_type = 1 + if self.sim.physics is None: + self.sim.physics = PhysxCfg() + self.sim.physics.solver_type = 1 # Render settings self.sim.render.enable_dlssg = True self.sim.render.enable_ambient_occlusion = True diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/track_goal_xarm_leap.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/track_goal_xarm_leap.py index dcce8aa2..db7b95d3 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/track_goal_xarm_leap.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/track_goal_xarm_leap.py @@ -173,12 +173,11 @@ def __post_init__(self): ): # this is necessary to visualize opacity in the raytracing import carb - import isaacsim.core.utils.carb as carb_utils self.sim.render.enable_translucency = True # # Access the Carb settings registry settings = carb.settings.get_settings() - carb_utils.set_carb_setting(settings, "/rtx/raytracing/fractionalCutoutOpacity", True) + settings.set("/rtx/raytracing/fractionalCutoutOpacity", True) @configclass diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/command.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/command.py index cc5c25ba..796378e3 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/command.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/command.py @@ -83,22 +83,23 @@ def _update_command(self): def _set_debug_vis_impl(self, debug_vis: bool): # create markers if necessary for the first tome - import isaacsim.core.utils.prims as prim_utils + from isaaclab.sim.utils import find_matching_prim_paths + from isaaclab.sim.utils.legacy import get_prim_at_path from pxr import UsdGeom if debug_vis: if not hasattr(self, "vis_articulation"): if self.cfg.articulation_vis_cfg.name in self.env.scene.keys(): # noqa: SIM118 self.vis_articulation: Articulation = self.env.scene[self.cfg.articulation_vis_cfg.name] - prims_paths = prim_utils.find_matching_prim_paths(self.vis_articulation.cfg.prim_path) - prims = [prim_utils.get_prim_at_path(prim) for prim in prims_paths] + prims_paths = find_matching_prim_paths(self.vis_articulation.cfg.prim_path) + prims = [get_prim_at_path(prim) for prim in prims_paths] for prim in prims: UsdGeom.Imageable(prim).MakeVisible() # VisualizationMarkers else: if hasattr(self, "vis_articulation"): - prims_paths = prim_utils.find_matching_prim_paths(self.vis_articulation.cfg.prim_path) - prims = [prim_utils.get_prim_at_path(prim) for prim in prims_paths] + prims_paths = find_matching_prim_paths(self.vis_articulation.cfg.prim_path) + prims = [get_prim_at_path(prim) for prim in prims_paths] for prim in prims: UsdGeom.Imageable(prim).MakeInvisible() diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/rewards.py index 8f53451f..d40761bd 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/rewards.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/rewards.py @@ -26,16 +26,16 @@ def stay_still( asset: Articulation = env.scene[hand_asset_cfg.name] ee_command = env.command_manager.get_command(ee_command_name) hand_command = env.command_manager.get_command(hand_command_name) - cur_hand_joint_pos = asset.data.joint_pos[:, hand_asset_cfg.joint_ids] + cur_hand_joint_pos = asset.data.joint_pos.torch[:, hand_asset_cfg.joint_ids] hand_error = torch.norm(hand_command - cur_hand_joint_pos, dim=1) des_pos_w, _ = math_utils.combine_frame_transforms( - asset.data.root_state_w[:, :3], asset.data.root_state_w[:, 3:7], ee_command[:, :3] + asset.data.root_state_w.torch[:, :3], asset.data.root_state_w.torch[:, 3:7], ee_command[:, :3] ) - curr_pos_w = asset.data.body_link_pos_w[:, ee_asset_cfg.body_ids, :3].view(-1, 3) + curr_pos_w = asset.data.body_link_pos_w.torch[:, ee_asset_cfg.body_ids, :3].view(-1, 3) distance = torch.norm(curr_pos_w - des_pos_w, dim=1) goal_reached_mask = (distance < 0.1) & (hand_error < 0.4) - return torch.where(goal_reached_mask, asset.data.body_vel_w.abs().sum(2).sum(1), 0) + return torch.where(goal_reached_mask, asset.data.body_vel_w.torch.abs().sum(2).sum(1), 0) def delta_action_l2( diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/track_goal_env.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/track_goal_env.py index e1f4b31f..fb2eb93c 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/track_goal_env.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/track_goal_env.py @@ -17,6 +17,7 @@ from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.scene import InteractiveSceneCfg from isaaclab.utils import configclass +from isaaclab_physx.physics import PhysxCfg from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR @@ -28,7 +29,7 @@ class SceneCfg(InteractiveSceneCfg): table = AssetBaseCfg( prim_path="{ENV_REGEX_NS}/Table", - init_state=AssetBaseCfg.InitialStateCfg(pos=(0.4, 0.0, -0.868), rot=(0.707, 0.0, 0.0, -0.707)), + init_state=AssetBaseCfg.InitialStateCfg(pos=(0.4, 0.0, -0.868), rot=(0.0, 0.0, -0.707, 0.707)), spawn=sim_utils.UsdFileCfg(usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Mounts/UWPatVention/pat_vention.usd"), ) @@ -193,9 +194,11 @@ def __post_init__(self): self.episode_length_s = 50 # simulation settings self.sim.dt = 0.02 / self.decimation - 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.gpu_max_rigid_patch_count = 5 * 2**16 + if self.sim.physics is None: + self.sim.physics = PhysxCfg() + self.sim.physics.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 + self.sim.physics.gpu_total_aggregate_pairs_capacity = 16 * 1024 + self.sim.physics.gpu_max_rigid_patch_count = 5 * 2**16 self.sim.render.enable_ambient_occlusion = True self.sim.render.enable_reflections = True diff --git a/tools/test_installer_pin.py b/tools/test_installer_pin.py new file mode 100644 index 00000000..88bfc3c8 --- /dev/null +++ b/tools/test_installer_pin.py @@ -0,0 +1,76 @@ +# Copyright (c) 2024-2026, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""CPU-only checks of the installer's pinned checkout and local-change guard.""" + +import os +import re +import shlex +import subprocess +import tempfile +import unittest +from pathlib import Path + +INSTALLER = (Path(__file__).resolve().parents[1] / "uwlab.sh").read_text() + + +class TestInstallerPin(unittest.TestCase): + """Exercise checkout selection with a fake Git command and no package installation.""" + + def _run_checkout(self, existing=False, dirty=False): + start = INSTALLER.index(' repo_root="${UWLAB_PATH}/_isaaclab/IsaacLab"') + checkout = re.search(r"^\s*git .* checkout .*FETCH_HEAD.*$", INSTALLER[start:], re.MULTILINE) + end = start + checkout.end() if checkout else INSTALLER.index(" ${pip_command}", start) + pin = re.search(r"^\s*ISAACLAB_COMMIT=.*$", INSTALLER[:start], re.MULTILINE) + fragment = (pin.group(0) + "\n" if pin else "") + INSTALLER[start:end] + self.assertNotIn("${pip_command}", fragment) + with tempfile.TemporaryDirectory(prefix="uwlab checkout ") as directory: + root = Path(directory) + if existing: + (root / "_isaaclab/IsaacLab/.git").mkdir(parents=True) + trace = root / "git.log" + script = "\n".join([ + "set -e", + f"UWLAB_PATH={shlex.quote(directory)}", + f"TRACE={shlex.quote(str(trace))}", + "git() {", + ' printf "%s\\t" "$@" >> "$TRACE"', + ' printf "\\n" >> "$TRACE"', + ' if [ "${3:-}" = status ]; then printf "%s" "${TEST_DIRTY:-}"; fi', + " return 0", + "}", + fragment, + ]) + env = os.environ.copy() + env.pop("UWLAB_ISAACLAB_COMMIT", None) + env["TEST_DIRTY"] = " M modified.py" if dirty else "" + result = subprocess.run(["bash", "-c", script], env=env, capture_output=True, text=True, check=False) + calls = [line.rstrip("\t").split("\t") for line in trace.read_text().splitlines()] if trace.exists() else [] + return result, calls + + def _assert_pinned_fetch(self, calls): + fetches = [call for call in calls if "fetch" in call] + self.assertEqual(len(fetches), 1) + self.assertRegex(fetches[0][-1], r"^[0-9a-f]{40}$") + self.assertTrue(any(call[-3:] == ["-q", "--detach", "FETCH_HEAD"] for call in calls)) + + def test_fresh_checkout_fetches_an_exact_revision(self): + result, calls = self._run_checkout() + self.assertEqual(result.returncode, 0, result.stderr) + self._assert_pinned_fetch(calls) + + def test_clean_existing_checkout_is_pinned(self): + result, calls = self._run_checkout(existing=True) + self.assertEqual(result.returncode, 0, result.stderr) + self._assert_pinned_fetch(calls) + + def test_dirty_existing_checkout_is_preserved(self): + result, calls = self._run_checkout(existing=True, dirty=True) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(any("fetch" in call or "checkout" in call for call in calls)) + + +if __name__ == "__main__": + unittest.main() diff --git a/uwlab.sh b/uwlab.sh index 4bfa38ae..5b29e240 100755 --- a/uwlab.sh +++ b/uwlab.sh @@ -43,50 +43,6 @@ install_system_deps() { fi } -# Returns success (exit code 0 / "true") if the detected Isaac Sim version starts with 4.5, -# otherwise returns non-zero ("false"). Works with both symlinked binary installs and pip installs. -is_isaacsim_version_4_5() { - local version="" - local python_exe - python_exe=$(extract_python_exe) - - # 0) Fast path: read VERSION file from the symlinked _isaac_sim directory (binary install) - # If the repository has _isaac_sim → symlink, the VERSION file is the simplest source of truth. - if [[ -f "${UWLAB_PATH}/_isaac_sim/VERSION" ]]; then - # Read first line of the VERSION file; don't fail the whole script on errors. - version=$(head -n1 "${UWLAB_PATH}/_isaac_sim/VERSION" || true) - fi - - # 1) Package-path probe: import isaacsim and walk up to ../../VERSION (pip or nonstandard layouts) - # If we still don't know the version, ask Python where the isaacsim package lives - if [[ -z "$version" ]]; then - local sim_file="" - # Print isaacsim.__file__; suppress errors so set -e won't abort. - sim_file=$("${python_exe}" -c 'import isaacsim, os; print(isaacsim.__file__)' 2>/dev/null || true) - if [[ -n "$sim_file" ]]; then - local version_path - version_path="$(dirname "$sim_file")/../../VERSION" - # If that VERSION file exists, read it. - [[ -f "$version_path" ]] && version=$(head -n1 "$version_path" || true) - fi - fi - - # 2) Fallback: use package metadata via importlib.metadata.version("isaacsim") - if [[ -z "$version" ]]; then - version=$("${python_exe}" <<'PY' 2>/dev/null || true -from importlib.metadata import version, PackageNotFoundError -try: - print(version("isaacsim")) -except PackageNotFoundError: - pass -PY -) - fi - - # Final decision: return success if version begins with "4.5", 0 if match, 1 otherwise. - [[ "$version" == 4.5* ]] -} - # check if running in docker is_docker() { [ -f /.dockerenv ] || \ @@ -111,12 +67,17 @@ ensure_cuda_torch() { # choose pins per arch local torch_ver tv_ver cuda_ver if is_arm; then - torch_ver="2.9.0" - tv_ver="0.24.0" + torch_ver="2.11.0" + tv_ver="0.26.0" cuda_ver="130" else - torch_ver="2.7.0" - tv_ver="0.22.0" + # isaacsim-core 6.1 (Isaac Lab 3.0) requires torch==2.11.0. Its default + # PyPI wheel is built against CUDA 13.0 and silently yields + # torch.cuda.is_available() == False on CUDA 12.x drivers, so this pin is + # re-applied after the Isaac Lab install below (the +cu128 suffix check + # catches the swap). Bump together with ISAACLAB_COMMIT. + torch_ver="2.11.0" + tv_ver="0.26.0" cuda_ver="128" fi @@ -356,20 +317,7 @@ setup_conda_env() { echo -e "[INFO] Creating conda environment named '${env_name}'..." echo -e "[INFO] Installing dependencies from ${UWLAB_PATH}/environment.yml" - # patch Python version if needed, but back up first - cp "${UWLAB_PATH}/environment.yml"{,.bak} - if is_isaacsim_version_4_5; then - echo "[INFO] Detected Isaac Sim 4.5 → forcing python=3.10" - sed -i 's/^ - python=3\.11/ - python=3.10/' "${UWLAB_PATH}/environment.yml" - else - echo "[INFO] Isaac Sim >= 5.0 detected, installing python=3.11" - fi - conda env create -y --file ${UWLAB_PATH}/environment.yml -n ${env_name} - # (optional) restore original environment.yml: - if [[ -f "${UWLAB_PATH}/environment.yml.bak" ]]; then - mv "${UWLAB_PATH}/environment.yml.bak" "${UWLAB_PATH}/environment.yml" - fi fi # cache current paths for later @@ -582,23 +530,85 @@ while [[ $# -gt 0 ]]; do export -f extract_pip_command export -f extract_pip_uninstall_command export -f install_uwlab_extension - # --- NEW: install upstream isaaclab (GitHub main, editable) --- - echo "[INFO] Installing upstream IsaacLab packages from GitHub (main) in editable mode into ${UWLAB_PATH}/_isaaclab ..." + # --- install upstream isaaclab (pinned commit, editable) --- + # + # Pinned, not tracking main: Isaac Lab and rsl-rl-lib move together + # (isaaclab_rl's cfg schema follows the rsl-rl API), so the commit here + # and the rsl-rl-lib commit in source/uwlab_rl/setup.py must be bumped + # as a pair. Override for experiments with UWLAB_ISAACLAB_COMMIT=. + ISAACLAB_COMMIT="${UWLAB_ISAACLAB_COMMIT:-ae37b028ea415c91ea2bc32609efcd759ed2b974}" # 3.0.0-EA + echo "[INFO] Installing upstream IsaacLab (pinned ${ISAACLAB_COMMIT:0:9}) in editable mode into ${UWLAB_PATH}/_isaaclab ..." repo_root="${UWLAB_PATH}/_isaaclab/IsaacLab" mkdir -p "${UWLAB_PATH}/_isaaclab" if [ ! -d "${repo_root}/.git" ]; then - echo "[INFO] Cloning IsaacLab repository (branch: main) into ${repo_root} ..." - git clone --depth 1 --branch main https://github.com/isaac-sim/IsaacLab.git "${repo_root}" + echo "[INFO] Initializing IsaacLab repository at ${repo_root} ..." + git init -q "${repo_root}" + git -C "${repo_root}" remote add origin https://github.com/isaac-sim/IsaacLab.git + elif [ -n "$(git -C "${repo_root}" status --porcelain)" ]; then + echo "[ERROR] IsaacLab checkout has local changes; refusing to replace them." + exit 1 else - echo "[INFO] Found existing IsaacLab clone at ${repo_root}; using it." + echo "[INFO] Found existing IsaacLab clone at ${repo_root}; checking out the pin." + fi + git -C "${repo_root}" fetch -q --depth 1 origin "${ISAACLAB_COMMIT}" + git -C "${repo_root}" checkout -q --detach FETCH_HEAD + + # Fail here rather than at the first training run: uwlab_rl targets the + # rsl-rl-lib >= 5.0 API, whose Isaac Lab side is recognisable by the + # `optimizer` field on RslRlPpoAlgorithmCfg. + if ! grep -q '^ optimizer' \ + "${repo_root}/source/isaaclab_rl/isaaclab_rl/rsl_rl/rl_cfg.py" 2>/dev/null; then + echo "[ERROR] Pinned IsaacLab (${ISAACLAB_COMMIT:0:9}) predates rsl-rl-lib 5.0 (no 'optimizer' field in" + echo "[ERROR] RslRlPpoAlgorithmCfg) but source/uwlab_rl/setup.py targets the 5.0+ API. Bump both pins together." + exit 1 fi - ${pip_command} -e "${repo_root}/source/isaaclab" --extra-index-url https://pypi.nvidia.com - ${pip_command} -e "${repo_root}/source/isaaclab_assets" --extra-index-url https://pypi.nvidia.com - ${pip_command} -e "${repo_root}/source/isaaclab_tasks" --extra-index-url https://pypi.nvidia.com - ${pip_command} -e "${repo_root}/source/isaaclab_rl[all]" --extra-index-url https://pypi.nvidia.com + # Isaac Lab 3.0 splits the core into per-backend packages; isaaclab + # imports isaaclab_physx/isaaclab_newton/isaaclab_ov at runtime, so all + # of them are required even for a single-backend install. + requirements_file=$(mktemp) + "${python_exe}" - "${repo_root}" "${UWLAB_PATH}" "${2:-all}" > "${requirements_file}" <<'PY' +import platform +import re +import sys +import tomllib +from pathlib import Path + +isaaclab_root, uwlab_root = (Path(value).resolve() for value in sys.argv[1:3]) +framework = sys.argv[3].replace("_", "-") +metadata = tomllib.loads((isaaclab_root / "pyproject.toml").read_text()) +project = metadata["project"] +sources = metadata["tool"]["uv"]["sources"] +requirements = list(project["dependencies"]) +for extra in ("isaacsim", "video"): + requirements.extend(project["optional-dependencies"][extra]) +if framework not in ("all", "none", "rsl-rl"): + requirements.extend(project["optional-dependencies"][framework]) +for requirement in dict.fromkeys(requirements): + name = re.match(r"[A-Za-z0-9_.-]+", requirement).group().replace("_", "-").lower() + source = sources.get(name) + if isinstance(source, dict) and "path" in source: + print("-e " + (isaaclab_root / source["path"]).resolve().as_uri()) + else: + print(requirement) +for extension in sorted((uwlab_root / "source").iterdir()): + if not (extension / "setup.py").is_file(): + continue + extra = "" + if extension.name == "uwlab_rl" and framework != "none": + extra = f"[{framework}]" + if extension.name == "uwlab_tasks" and platform.machine() == "x86_64": + extra = "[collision]" + print("-e " + extension.as_uri() + extra) +PY + ${pip_command} -r "${requirements_file}" --extra-index-url https://pypi.nvidia.com + rm "${requirements_file}" echo "[INFO] Upstream IsaacLab packages installed (editable) from local clone at ${repo_root}." # source directory find -L "${UWLAB_PATH}/source" -mindepth 1 -maxdepth 1 -type d -exec bash -c 'install_uwlab_extension "{}"' \; + # collision analyzer (dataset generation) needs pytorch3d; prebuilt wheels exist for linux x86_64 only + if [ "$(uname -m)" = "x86_64" ]; then + ${pip_command} -e "${UWLAB_PATH}/source/uwlab_tasks[collision]" + fi # install the python packages for supported reinforcement learning frameworks echo "[INFO] Installing extra requirements such as learning frameworks..." # check if specified which rl-framework to install